'accessiq-analytics', 'days' => absint( $days ), '_wpnonce' => wp_create_nonce( self::ANALYTICS_DAYS_NONCE_ACTION ), ), admin_url( 'admin.php' ) ); } /** * Initialize hooks. */ public function init() { add_action( 'admin_menu', array( $this, 'add_analytics_pages' ), 20 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); // Heatmap AJAX add_action( 'wp_ajax_accessiq_get_heatmap_data', array( $this, 'ajax_get_heatmap_data' ) ); // Reports AJAX add_action( 'wp_ajax_accessiq_get_report_summary', array( $this, 'ajax_get_report_summary' ) ); add_action( 'wp_ajax_accessiq_get_daily_sessions', array( $this, 'ajax_get_daily_sessions' ) ); add_action( 'wp_ajax_accessiq_get_page_reports', array( $this, 'ajax_get_page_reports' ) ); // Journey Tracker AJAX add_action( 'wp_ajax_accessiq_get_journey_sessions', array( $this, 'ajax_get_journey_sessions' ) ); add_action( 'wp_ajax_accessiq_get_journey_detail', array( $this, 'ajax_get_journey_detail' ) ); // Statement Generator AJAX add_action( 'wp_ajax_accessiq_publish_statement', array( $this, 'ajax_publish_statement' ) ); // Admin-post handlers (CSV export + clear data) add_action( 'admin_post_accessiq_export_clicks_csv', array( $this, 'export_clicks_csv' ) ); add_action( 'admin_post_accessiq_export_sessions_csv', array( $this, 'export_sessions_csv' ) ); add_action( 'admin_post_accessiq_clear_clicks', array( $this, 'clear_clicks' ) ); add_action( 'admin_post_accessiq_clear_sessions', array( $this, 'clear_sessions' ) ); } /** * Add analytics subpages under the AccessIQ menu. */ public function add_analytics_pages() { // ── Accessibility group ────────────────────────────────── add_submenu_page( 'accessiq', __( 'WCAG Scanner', 'accessiq' ), __( '🔬 WCAG Scanner', 'accessiq' ), 'manage_options', 'accessiq-scanner', array( AccessIQ_Settings::instance(), 'render_scanner_page' ) ); add_submenu_page( 'accessiq', __( 'Scan Reports', 'accessiq' ), __( '📋 Scan Reports', 'accessiq' ), 'manage_options', 'accessiq-reports', array( AccessIQ_Settings::instance(), 'render_reports_page' ) ); // ── InsightForge group ─────────────────────────────────── add_submenu_page( 'accessiq', __( 'Heatmap', 'accessiq' ), __( '🔥 Heatmap', 'accessiq' ), 'manage_options', 'accessiq-heatmap', array( $this, 'render_heatmap_page' ) ); add_submenu_page( 'accessiq', __( 'Behavior Analytics', 'accessiq' ), __( '📈 Behavior', 'accessiq' ), 'manage_options', 'accessiq-analytics', array( $this, 'render_analytics_page' ) ); add_submenu_page( 'accessiq', __( 'Journey Tracker', 'accessiq' ), __( '🗺️ Journey', 'accessiq' ), 'manage_options', 'accessiq-journey', array( $this, 'render_journey_page' ) ); // ── Statement Generator ────────────────────────────────── add_submenu_page( 'accessiq', __( 'Accessibility Statement', 'accessiq' ), __( '📄 Statement', 'accessiq' ), 'manage_options', 'accessiq-statement', array( $this, 'render_statement_page' ) ); // ── Bottom items ───────────────────────────────────────── add_submenu_page( 'accessiq', __( 'About AccessIQ', 'accessiq' ), __( 'ℹ️ About', 'accessiq' ), 'manage_options', 'accessiq-about', array( $this, 'render_about_page' ) ); add_submenu_page( 'accessiq', __( 'Settings', 'accessiq' ), __( '⚙️ Settings', 'accessiq' ), 'manage_options', 'accessiq-settings', array( $this, 'render_settings_stub' ) ); if ( ! apply_filters( 'accessiq_has_pro', false ) ) { add_submenu_page( 'accessiq', __( 'Upgrade to Pro', 'accessiq' ), '⚡ ' . __( 'Upgrade to Pro', 'accessiq' ), 'manage_options', 'accessiq-upgrade', 'accessiq_render_upgrade_page' ); } } /** * Render the About page. */ public function render_statement_page() { include ACCESSIQ_DIR . 'admin/views/statement.php'; } /** * AJAX: Publish (create/update) the Accessibility Statement page. */ public function ajax_publish_statement() { check_ajax_referer( 'accessiq_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'accessiq' ) ), 403 ); } $content = isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : ''; if ( empty( trim( $content ) ) ) { wp_send_json_error( array( 'message' => __( 'Statement content is empty.', 'accessiq' ) ) ); } $existing_id = (int) get_option( 'accessiq_statement_page_id', 0 ); $page_data = array( 'post_title' => __( 'Accessibility Statement', 'accessiq' ), 'post_content' => $content, 'post_status' => 'publish', 'post_type' => 'page', ); if ( $existing_id && get_post( $existing_id ) ) { $page_data['ID'] = $existing_id; $page_id = wp_update_post( $page_data, true ); } else { $page_data['post_name'] = 'accessibility-statement'; $page_id = wp_insert_post( $page_data, true ); } if ( is_wp_error( $page_id ) ) { wp_send_json_error( array( 'message' => $page_id->get_error_message() ) ); } update_option( 'accessiq_statement_page_id', $page_id ); wp_send_json_success( array( 'message' => __( 'Accessibility Statement published!', 'accessiq' ), 'page_url' => get_permalink( $page_id ), 'page_id' => $page_id, ) ); } public function render_about_page() { require_once ACCESSIQ_DIR . 'admin/views/about.php'; } public function render_heatmap_page() { include ACCESSIQ_DIR . 'admin/views/heatmap.php'; } public function render_analytics_page() { include ACCESSIQ_DIR . 'admin/views/analytics.php'; } public function render_journey_page() { include ACCESSIQ_DIR . 'admin/views/journey.php'; } public function render_settings_stub() { include ACCESSIQ_DIR . 'admin/views/settings.php'; } /** * Build daily session chart series for Behavior Analytics (matches admin/views/analytics.php). * * @return array{labels: string[], values: int[]} */ private function get_sessions_chart_series_for_enqueue() { global $wpdb; $days = self::get_validated_analytics_days(); $sessions_tbl = $wpdb->prefix . 'accessiq_sessions'; $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $days . ' days', current_time( 'timestamp' ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $daily = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT DATE(created_at) AS day, COUNT(*) AS sessions FROM `' . esc_sql( $sessions_tbl ) . '` WHERE created_at >= %s GROUP BY DATE(created_at) ORDER BY day ASC', $cutoff ) ); $by_date = array(); foreach ( (array) $daily as $row ) { $by_date[ $row->day ] = (int) $row->sessions; } $labels = array(); $values = array(); for ( $i = $days - 1; $i >= 0; $i-- ) { $ts = strtotime( '-' . $i . ' days' ); $date = gmdate( 'Y-m-d', $ts ); $labels[] = gmdate( 'M d', $ts ); $values[] = isset( $by_date[ $date ] ) ? $by_date[ $date ] : 0; } return array( 'labels' => $labels, 'values' => $values, ); } /** * Enqueue analytics-specific admin assets. */ public function enqueue_assets( $hook ) { if ( strpos( $hook, 'accessiq' ) === false ) { return; } // Always load chart.js on AccessIQ pages wp_enqueue_script( 'accessiq-chartjs', ACCESSIQ_URL . 'admin/js/chart.min.js', array(), '4.4.8', true ); if ( 'accessiq_page_accessiq-heatmap' === $hook ) { wp_enqueue_script( 'accessiq-heatmap-lib', ACCESSIQ_URL . 'admin/js/heatmap.min.js', array(), '2.0.2', true ); wp_enqueue_script( 'accessiq-heatmap-admin', ACCESSIQ_URL . 'admin/js/heatmap-admin.js', array( 'jquery', 'accessiq-heatmap-lib' ), ACCESSIQ_VERSION, true ); wp_localize_script( 'accessiq-heatmap-admin', 'accessiqHeatmapData', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessiq_admin_nonce' ), 'strings' => array( 'error' => __( 'Failed to generate heatmap.', 'accessiq' ), 'noData' => __( 'No click data yet for this page.', 'accessiq' ), ), ) ); } if ( 'accessiq_page_accessiq-journey' === $hook ) { wp_enqueue_style( 'accessiq-analytics-view', ACCESSIQ_URL . 'admin/css/accessiq-analytics-view.css', array( 'accessiq-admin-sidebar' ), ACCESSIQ_VERSION ); wp_enqueue_script( 'accessiq-journey', ACCESSIQ_URL . 'admin/js/accessiq-journey.js', array( 'jquery' ), ACCESSIQ_VERSION, true ); wp_localize_script( 'accessiq-journey', 'accessiqJourneyData', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessiq_reports_nonce' ), 'strings' => array( 'loading' => __( 'Loading journey...', 'accessiq' ), 'error' => __( 'Failed to load journey.', 'accessiq' ), 'noData' => __( 'No page views found for this session.', 'accessiq' ), 'exit' => __( 'exit', 'accessiq' ), ), ) ); } if ( 'accessiq_page_accessiq-analytics' === $hook ) { wp_enqueue_style( 'accessiq-analytics-view', ACCESSIQ_URL . 'admin/css/accessiq-analytics-view.css', array( 'accessiq-admin-sidebar' ), ACCESSIQ_VERSION ); wp_enqueue_script( 'accessiq-analytics-sessions-chart', ACCESSIQ_URL . 'admin/js/accessiq-analytics-sessions-chart.js', array( 'accessiq-chartjs' ), ACCESSIQ_VERSION, true ); wp_localize_script( 'accessiq-analytics-sessions-chart', 'accessiqSessionsChart', $this->get_sessions_chart_series_for_enqueue() ); wp_localize_script( 'accessiq-analytics-sessions-chart', 'accessiqAnalytics', $this->get_analytics_localize_data() ); wp_enqueue_script( 'accessiq-analytics-reports', ACCESSIQ_URL . 'admin/js/analytics-reports.js', array( 'jquery', 'accessiq-chartjs' ), ACCESSIQ_VERSION, true ); wp_localize_script( 'accessiq-analytics-reports', 'accessiqReportsData', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessiq_reports_nonce' ), 'strings' => array( 'noData' => __( 'No data yet — tracking will begin automatically.', 'accessiq' ), 'loading' => __( 'Loading reports...', 'accessiq' ), 'error' => __( 'Error fetching report data.', 'accessiq' ), ), ) ); } } /** * Register analytics settings. */ public function register_settings() { $accessiq_options = array( array( 'id' => 'accessiq_analytics_enable_click_tracking', 'type' => 'boolean', 'default' => true ), array( 'id' => 'accessiq_analytics_enable_behavior_tracking', 'type' => 'boolean', 'default' => true ), array( 'id' => 'accessiq_analytics_exclude_logged_in', 'type' => 'boolean', 'default' => false ), array( 'id' => 'accessiq_analytics_exclude_admin', 'type' => 'boolean', 'default' => true ), array( 'id' => 'accessiq_analytics_replace_dashboard', 'type' => 'boolean', 'default' => false ), array( 'id' => 'accessiq_analytics_dashboard_roles', 'type' => 'array', 'default' => array( 'administrator' ), 'sanitize_callback' => array( $this, 'sanitize_dashboard_roles' ), ), array( 'id' => 'accessiq_analytics_data_retention', 'type' => 'integer', 'default' => 90 ), ); foreach ( $accessiq_options as $accessiq_option ) { if ( isset( $accessiq_option['sanitize_callback'] ) && ! empty( $accessiq_option['sanitize_callback'] ) ) { $accessiq_sanitize = $accessiq_option['sanitize_callback']; } elseif ( isset( $accessiq_option['type'] ) && 'boolean' === $accessiq_option['type'] ) { $accessiq_sanitize = 'rest_sanitize_boolean'; } elseif ( isset( $accessiq_option['type'] ) && 'integer' === $accessiq_option['type'] ) { $accessiq_sanitize = 'absint'; } else { $accessiq_sanitize = 'sanitize_text_field'; } register_setting( 'accessiq_analytics_settings_group', $accessiq_option['id'], array( 'type' => $accessiq_option['type'], 'default' => $accessiq_option['default'], 'sanitize_callback' => $accessiq_sanitize, ) ); } } /** * Sanitize dashboard roles array. */ public function sanitize_dashboard_roles( $input ) { $accessiq_allowed = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' ); $accessiq_clean = array(); if ( is_array( $input ) ) { foreach ( $input as $accessiq_role ) { if ( in_array( $accessiq_role, $accessiq_allowed, true ) ) { $accessiq_clean[] = $accessiq_role; } } } return empty( $accessiq_clean ) ? array( 'administrator' ) : $accessiq_clean; } // ── AJAX: Heatmap Data ───────────────────────────────────────────────── public function ajax_get_heatmap_data() { check_ajax_referer( 'accessiq_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } global $wpdb; $page_url_raw = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : ( isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '' ); $url = trailingslashit( $page_url_raw ? $page_url_raw : '' ); $url_normalized = rtrim( $url, '/' ); $date_range = isset( $_POST['date_range'] ) ? absint( $_POST['date_range'] ) : 7; $device_type = isset( $_POST['device_type'] ) ? sanitize_text_field( wp_unslash( $_POST['device_type'] ) ) : 'all'; $clicks_table = $wpdb->prefix . 'accessiq_clicks'; $date_start = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $date_range . ' days' ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching if ( 'all' !== $device_type ) { $cache_key_stats = 'accessiq_heatmap_stats_' . $date_range . '_' . $device_type . '_' . md5( $url ); $accessiq_stats = wp_cache_get( $cache_key_stats, 'accessiq' ); if ( false === $accessiq_stats ) { $accessiq_stats = $wpdb->get_row( $wpdb->prepare( 'SELECT COUNT(id) as total_clicks, (SELECT element FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND device_type = %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY element ORDER BY COUNT(id) DESC LIMIT 1) as top_element, (SELECT HOUR(created_at) FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND device_type = %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY HOUR(created_at) ORDER BY COUNT(id) DESC LIMIT 1) as peak_hour FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND device_type = %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s)', $date_start, $device_type, $url_normalized, $date_start, $device_type, $url_normalized, $date_start, $device_type, $url_normalized ) ); wp_cache_set( $cache_key_stats, $accessiq_stats, 'accessiq', 300 ); } $cache_key_clicks = 'accessiq_heatmap_clicks_' . $date_range . '_' . $device_type . '_' . md5( $url ); $accessiq_clicks = wp_cache_get( $cache_key_clicks, 'accessiq' ); if ( false === $accessiq_clicks ) { $accessiq_clicks = $wpdb->get_results( $wpdb->prepare( 'SELECT ROUND(x_percent/2)*2 as x_grid, ROUND(y_percent/2)*2 as y_grid, element, COUNT(id) as value FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND device_type = %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY x_grid, y_grid, element ORDER BY value DESC LIMIT 5000', $date_start, $device_type, $url_normalized ) ); wp_cache_set( $cache_key_clicks, $accessiq_clicks, 'accessiq', 300 ); } $cache_key_els = 'accessiq_heatmap_elements_' . $date_range . '_' . $device_type . '_' . md5( $url ); $accessiq_els = wp_cache_get( $cache_key_els, 'accessiq' ); if ( false === $accessiq_els ) { $accessiq_els = $wpdb->get_results( $wpdb->prepare( 'SELECT element, COUNT(id) as count FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND device_type = %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY element ORDER BY count DESC LIMIT 10', $date_start, $device_type, $url_normalized ) ); wp_cache_set( $cache_key_els, $accessiq_els, 'accessiq', 300 ); } } else { $cache_key_stats = 'accessiq_heatmap_stats_' . $date_range . '_all_' . md5( $url ); $accessiq_stats = wp_cache_get( $cache_key_stats, 'accessiq' ); if ( false === $accessiq_stats ) { $accessiq_stats = $wpdb->get_row( $wpdb->prepare( 'SELECT COUNT(id) as total_clicks, (SELECT element FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY element ORDER BY COUNT(id) DESC LIMIT 1) as top_element, (SELECT HOUR(created_at) FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY HOUR(created_at) ORDER BY COUNT(id) DESC LIMIT 1) as peak_hour FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s)', $date_start, $url_normalized, $date_start, $url_normalized, $date_start, $url_normalized ) ); wp_cache_set( $cache_key_stats, $accessiq_stats, 'accessiq', 300 ); } $cache_key_clicks = 'accessiq_heatmap_clicks_' . $date_range . '_all_' . md5( $url ); $accessiq_clicks = wp_cache_get( $cache_key_clicks, 'accessiq' ); if ( false === $accessiq_clicks ) { $accessiq_clicks = $wpdb->get_results( $wpdb->prepare( 'SELECT ROUND(x_percent/2)*2 as x_grid, ROUND(y_percent/2)*2 as y_grid, element, COUNT(id) as value FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY x_grid, y_grid, element ORDER BY value DESC LIMIT 5000', $date_start, $url_normalized ) ); wp_cache_set( $cache_key_clicks, $accessiq_clicks, 'accessiq', 300 ); } $cache_key_els = 'accessiq_heatmap_elements_' . $date_range . '_all_' . md5( $url ); $accessiq_els = wp_cache_get( $cache_key_els, 'accessiq' ); if ( false === $accessiq_els ) { $accessiq_els = $wpdb->get_results( $wpdb->prepare( 'SELECT element, COUNT(id) as count FROM `' . esc_sql( $clicks_table ) . '` WHERE created_at >= %s AND TRIM(TRAILING \'/\' FROM page_url) = TRIM(TRAILING \'/\' FROM %s) GROUP BY element ORDER BY count DESC LIMIT 10', $date_start, $url_normalized ) ); wp_cache_set( $cache_key_els, $accessiq_els, 'accessiq', 300 ); } } // phpcs:enable wp_send_json_success( array( 'clicks' => $accessiq_clicks, 'total' => isset( $accessiq_stats->total_clicks ) ? (int) $accessiq_stats->total_clicks : 0, 'top_element' => isset( $accessiq_stats->top_element ) && $accessiq_stats->top_element ? $accessiq_stats->top_element : '-', 'peak_hour' => isset( $accessiq_stats->peak_hour ) ? $accessiq_stats->peak_hour . ':00' : '-', 'top_elements' => $accessiq_els, ) ); } // ── AJAX: Report Summary ─────────────────────────────────────────────── public function ajax_get_report_summary() { check_ajax_referer( 'accessiq_reports_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } global $wpdb; $date_range = isset( $_POST['date_range'] ) ? absint( $_POST['date_range'] ) : 30; $device = isset( $_POST['device_type'] ) ? sanitize_text_field( wp_unslash( $_POST['device_type'] ) ) : 'all'; $table = $wpdb->prefix . 'accessiq_sessions'; $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $date_range . ' days', current_time( 'timestamp' ) ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching $cache_key = 'accessiq_report_summary_' . $date_range . '_' . $device; $accessiq_total = wp_cache_get( $cache_key, 'accessiq' ); if ( false === $accessiq_total ) { if ( 'all' !== $device ) { $accessiq_total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s", $cutoff, $device ) ); } else { $accessiq_total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s", $cutoff ) ); } wp_cache_set( $cache_key, $accessiq_total, 'accessiq', 300 ); } $cache_key2 = 'accessiq_report_avg_' . $date_range . '_' . $device; $accessiq_avg = wp_cache_get( $cache_key2, 'accessiq' ); if ( false === $accessiq_avg ) { if ( 'all' !== $device ) { $accessiq_avg = $wpdb->get_var( $wpdb->prepare( "SELECT AVG(time_spent) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s", $cutoff, $device ) ); } else { $accessiq_avg = $wpdb->get_var( $wpdb->prepare( "SELECT AVG(time_spent) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s", $cutoff ) ); } wp_cache_set( $cache_key2, $accessiq_avg, 'accessiq', 300 ); } $cache_key3 = 'accessiq_report_bounce_' . $date_range . '_' . $device; $accessiq_bounce = wp_cache_get( $cache_key3, 'accessiq' ); if ( false === $accessiq_bounce ) { if ( 'all' !== $device ) { $accessiq_bounce = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s AND is_bounce = 1", $cutoff, $device ) ); } else { $accessiq_bounce = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND is_bounce = 1", $cutoff ) ); } wp_cache_set( $cache_key3, $accessiq_bounce, 'accessiq', 300 ); } $accessiq_bounce_rate = $accessiq_total > 0 ? round( ( $accessiq_bounce / $accessiq_total ) * 100, 2 ) : 0; $cache_key4 = 'accessiq_report_device_' . $date_range . '_' . $device; $accessiq_top_device = wp_cache_get( $cache_key4, 'accessiq' ); if ( false === $accessiq_top_device ) { if ( 'all' !== $device ) { $accessiq_top_device = $wpdb->get_var( $wpdb->prepare( "SELECT device_type FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s GROUP BY device_type ORDER BY COUNT(id) DESC LIMIT 1", $cutoff, $device ) ); } else { $accessiq_top_device = $wpdb->get_var( $wpdb->prepare( "SELECT device_type FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s GROUP BY device_type ORDER BY COUNT(id) DESC LIMIT 1", $cutoff ) ); } wp_cache_set( $cache_key4, $accessiq_top_device, 'accessiq', 300 ); } // phpcs:enable $accessiq_minutes = floor( $accessiq_avg / 60 ); $accessiq_seconds = round( $accessiq_avg % 60 ); wp_send_json_success( array( 'total_sessions' => (int) $accessiq_total, 'avg_time' => "{$accessiq_minutes}m {$accessiq_seconds}s", 'bounce_rate' => $accessiq_bounce_rate, 'top_device' => $accessiq_top_device ? ucfirst( $accessiq_top_device ) : '-', ) ); } // ── AJAX: Daily Sessions ─────────────────────────────────────────────── public function ajax_get_daily_sessions() { check_ajax_referer( 'accessiq_reports_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } global $wpdb; $date_range = isset( $_POST['date_range'] ) ? absint( $_POST['date_range'] ) : 30; $device = isset( $_POST['device_type'] ) ? sanitize_text_field( wp_unslash( $_POST['device_type'] ) ) : 'all'; $table = $wpdb->prefix . 'accessiq_sessions'; $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $date_range . ' days', current_time( 'timestamp' ) ) ); $cache_key = 'accessiq_daily_sessions_' . $date_range . '_' . $device; $accessiq_results = wp_cache_get( $cache_key, 'accessiq' ); if ( false === $accessiq_results ) { if ( 'all' !== $device ) { $accessiq_query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT DATE(created_at) as cdate, COUNT(id) as count FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s GROUP BY cdate ORDER BY cdate ASC", $cutoff, $device ); } else { $accessiq_query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT DATE(created_at) as cdate, COUNT(id) as count FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s GROUP BY cdate ORDER BY cdate ASC", $cutoff ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $accessiq_results = $wpdb->get_results( $accessiq_query ); wp_cache_set( $cache_key, $accessiq_results, 'accessiq', 300 ); } $accessiq_map = array(); foreach ( $accessiq_results as $r ) { $accessiq_map[ $r->cdate ] = (int) $r->count; } $accessiq_chart = array(); $accessiq_start = strtotime( "-{$date_range} days" ); for ( $i = 0; $i < $date_range; $i++ ) { $accessiq_date = gmdate( 'Y-m-d', strtotime( "+{$i} days", $accessiq_start ) ); $accessiq_chart[] = array( 'date' => gmdate( 'M d', strtotime( $accessiq_date ) ), 'count' => isset( $accessiq_map[ $accessiq_date ] ) ? $accessiq_map[ $accessiq_date ] : 0, ); } wp_send_json_success( $accessiq_chart ); } // ── AJAX: Page Reports ───────────────────────────────────────────────── public function ajax_get_page_reports() { check_ajax_referer( 'accessiq_reports_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } global $wpdb; $date_range = isset( $_POST['date_range'] ) ? absint( $_POST['date_range'] ) : 30; $device = isset( $_POST['device_type'] ) ? sanitize_text_field( wp_unslash( $_POST['device_type'] ) ) : 'all'; $table = $wpdb->prefix . 'accessiq_sessions'; $cache_key = 'accessiq_page_reports_' . $date_range . '_' . $device; $accessiq_pages = wp_cache_get( $cache_key, 'accessiq' ); if ( false === $accessiq_pages ) { $not_wp_content = '%' . $wpdb->esc_like( '/wp-content/' ) . '%'; $not_wp_admin = '%' . $wpdb->esc_like( '/wp-admin/' ) . '%'; $not_wp_login = '%' . $wpdb->esc_like( '/wp-login' ) . '%'; $not_wp_json = '%' . $wpdb->esc_like( '/wp-json/' ) . '%'; $not_php = '%' . $wpdb->esc_like( '.php' ) . '%'; $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $date_range . ' days', current_time( 'timestamp' ) ) ); if ( 'all' !== $device ) { $accessiq_query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT page_url, COUNT(id) as sessions, AVG(time_spent) as avg_time, SUM(is_bounce) as bounce_count FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND device_type = %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s GROUP BY page_url ORDER BY sessions DESC LIMIT 10", $cutoff, $device, $not_wp_content, $not_wp_admin, $not_wp_login, $not_wp_json, $not_php ); } else { $accessiq_query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT page_url, COUNT(id) as sessions, AVG(time_spent) as avg_time, SUM(is_bounce) as bounce_count FROM `" . esc_sql( $table ) . "` WHERE created_at >= %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s AND page_url NOT LIKE %s GROUP BY page_url ORDER BY sessions DESC LIMIT 10", $cutoff, $not_wp_content, $not_wp_admin, $not_wp_login, $not_wp_json, $not_php ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared $accessiq_pages = $wpdb->get_results( $accessiq_query ); wp_cache_set( $cache_key, $accessiq_pages, 'accessiq', 300 ); } $accessiq_response = array(); foreach ( $accessiq_pages as $p ) { $accessiq_parsed = wp_parse_url( $p->page_url ); $accessiq_path = isset( $accessiq_parsed['path'] ) ? $accessiq_parsed['path'] : $p->page_url; if ( isset( $accessiq_parsed['query'] ) ) { $accessiq_path .= '?' . $accessiq_parsed['query']; } $accessiq_bounce_rate = $p->sessions > 0 ? round( ( $p->bounce_count / $p->sessions ) * 100, 2 ) : 0; $accessiq_mins = floor( $p->avg_time / 60 ); $accessiq_secs = round( $p->avg_time % 60 ); $accessiq_response[] = array( 'page_url' => $accessiq_path, 'sessions' => $p->sessions, 'avg_time' => "{$accessiq_mins}m {$accessiq_secs}s", 'bounce_rate' => $accessiq_bounce_rate, ); } wp_send_json_success( $accessiq_response ); } // ── Journey Tracker AJAX ────────────────────────────────────────────── /** * Return paginated session summary for the Journey list. */ public function ajax_get_journey_sessions() { check_ajax_referer( 'accessiq_reports_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } global $wpdb; $pv_table = $wpdb->prefix . 'accessiq_page_views'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT session_id, COUNT(id) AS page_count, SUM(time_spent) AS total_time, MIN(created_at) AS first_visit FROM `' . esc_sql( $pv_table ) . '` GROUP BY session_id ORDER BY first_visit DESC LIMIT %d', 50 ) ); $result = array(); if ( ! empty( $rows ) ) { // Fetch all entry pages in one query — avoids N+1 per session. $session_ids = array_column( (array) $rows, 'session_id' ); $placeholders = implode( ', ', array_fill( 0, count( $session_ids ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $entry_rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT pv1.session_id, pv1.page_url FROM `' . esc_sql( $pv_table ) . '` pv1 INNER JOIN ( SELECT session_id, MIN(created_at) AS min_time FROM `' . esc_sql( $pv_table ) . '` WHERE session_id IN (' . $placeholders . ') GROUP BY session_id ) pv2 ON pv1.session_id = pv2.session_id AND pv1.created_at = pv2.min_time', ...$session_ids ) ); $entry_map = array(); foreach ( (array) $entry_rows as $er ) { $entry_map[ $er->session_id ] = $er->page_url; } foreach ( (array) $rows as $row ) { $entry = isset( $entry_map[ $row->session_id ] ) ? $entry_map[ $row->session_id ] : ''; $entry_path = $entry ? ( wp_parse_url( $entry, PHP_URL_PATH ) ?: $entry ) : '/'; $result[] = array( 'session_id' => $row->session_id, 'entry_page' => $entry_path, 'page_count' => (int) $row->page_count, 'total_time' => (int) $row->total_time, 'first_visit' => $row->first_visit, ); } } wp_send_json_success( $result ); } /** * Return all page views for a single session (journey timeline). */ public function ajax_get_journey_detail() { check_ajax_referer( 'accessiq_reports_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } $session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : ''; if ( empty( $session_id ) ) { wp_send_json_error( 'Missing session ID' ); } global $wpdb; $pv_table = $wpdb->prefix . 'accessiq_page_views'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT page_url, page_title, time_spent, scroll_depth, created_at FROM `' . esc_sql( $pv_table ) . '` WHERE session_id = %s ORDER BY created_at ASC', $session_id ) ); $views = array(); foreach ( (array) $rows as $r ) { $path = wp_parse_url( $r->page_url, PHP_URL_PATH ); $views[] = array( 'url' => $path ? $path : $r->page_url, 'title' => $r->page_title, 'time_spent' => (int) $r->time_spent, 'scroll_depth' => (int) $r->scroll_depth, 'created_at' => $r->created_at, ); } wp_send_json_success( $views ); } // ── CSV Export: Clicks ───────────────────────────────────────────────── public function export_clicks_csv() { if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'accessiq_export_nonce' ) ) { wp_die( esc_html__( 'Security check failed.', 'accessiq' ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'accessiq' ) ); } global $wpdb; $table = $wpdb->prefix . 'accessiq_clicks'; $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT * FROM `' . esc_sql( $table ) . '` ORDER BY created_at DESC LIMIT %d', 10000 ) ); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="accessiq-clicks-' . gmdate( 'Y-m-d' ) . '.csv"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $output = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( $output !== false ) { fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'ID', 'Page URL', 'X%', 'Y%', 'Element', 'Device', 'Session ID', 'Created At' ) ); foreach ( $results as $row ) { fputcsv( $output, array( $row->id, $row->page_url, $row->x_percent, $row->y_percent, $row->element, $row->device_type, $row->session_id, $row->created_at ) ); } fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } exit; } // ── CSV Export: Sessions ─────────────────────────────────────────────── public function export_sessions_csv() { if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'accessiq_export_nonce' ) ) { wp_die( esc_html__( 'Security check failed.', 'accessiq' ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'accessiq' ) ); } global $wpdb; $table = $wpdb->prefix . 'accessiq_sessions'; $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT * FROM `' . esc_sql( $table ) . '` ORDER BY created_at DESC LIMIT %d', 10000 ) ); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="accessiq-sessions-' . gmdate( 'Y-m-d' ) . '.csv"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $output = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( $output !== false ) { fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'ID', 'Session ID', 'Page URL', 'Time (sec)', 'Time (formatted)', 'Is Bounce', 'Referrer', 'Device', 'Date' ) ); foreach ( $results as $row ) { fputcsv( $output, array( $row->id, $row->session_id, $row->page_url, $row->time_spent, gmdate( 'H:i:s', $row->time_spent ), $row->is_bounce, $row->referrer, $row->device_type, $row->created_at ) ); } fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } exit; } // ── Clear Data ───────────────────────────────────────────────────────── public function clear_clicks() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'accessiq_clear_nonce' ) ) { wp_die( esc_html__( 'Security check failed.', 'accessiq' ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'accessiq' ) ); } global $wpdb; $wpdb->query( 'TRUNCATE TABLE ' . esc_sql( $wpdb->prefix . 'accessiq_clicks' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared wp_cache_flush_group( 'accessiq' ); wp_safe_redirect( add_query_arg( array( 'page' => 'accessiq-settings', 'tab' => 'analytics', 'cleared' => 'clicks', '_wpnonce' => wp_create_nonce( 'accessiq_cleared_action' ) ), admin_url( 'admin.php' ) ) ); exit; } /** * Gather device breakdown + session totals for analytics page JS. * * @return array */ private function get_analytics_localize_data() { global $wpdb; $days = self::get_validated_analytics_days(); $table = $wpdb->prefix . 'accessiq_sessions'; $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $days . ' days', current_time( 'timestamp' ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $accessiq_device_rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 'SELECT device_type, COUNT(*) AS cnt FROM `' . esc_sql( $table ) . '` WHERE created_at >= %s GROUP BY device_type', $cutoff ) ); $accessiq_desktop = 0; $accessiq_mobile = 0; $accessiq_tablet = 0; foreach ( (array) $accessiq_device_rows as $accessiq_dr ) { $accessiq_dt = strtolower( $accessiq_dr->device_type ); if ( 'desktop' === $accessiq_dt ) { $accessiq_desktop = (int) $accessiq_dr->cnt; } elseif ( 'mobile' === $accessiq_dt ) { $accessiq_mobile = (int) $accessiq_dr->cnt; } elseif ( 'tablet' === $accessiq_dt ) { $accessiq_tablet = (int) $accessiq_dr->cnt; } } $accessiq_total = $accessiq_desktop + $accessiq_mobile + $accessiq_tablet; return array( 'device_data' => array( 'desktop' => $accessiq_desktop, 'mobile' => $accessiq_mobile, 'tablet' => $accessiq_tablet, ), 'total_sessions' => $accessiq_total, 'mobile_sessions' => $accessiq_mobile, ); } public function clear_sessions() { if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'accessiq_clear_nonce' ) ) { wp_die( esc_html__( 'Security check failed.', 'accessiq' ) ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'accessiq' ) ); } global $wpdb; $wpdb->query( 'TRUNCATE TABLE ' . esc_sql( $wpdb->prefix . 'accessiq_sessions' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared wp_cache_flush_group( 'accessiq' ); wp_safe_redirect( add_query_arg( array( 'page' => 'accessiq-settings', 'tab' => 'analytics', 'cleared' => 'sessions', '_wpnonce' => wp_create_nonce( 'accessiq_cleared_action' ) ), admin_url( 'admin.php' ) ) ); exit; } }