true] ); } if (!$license_domain) { update_option('twhcp_license_domain', $current_domain); } // Database Table Creation global $wpdb; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $table_name = $wpdb->prefix . 'twhcp_heatmap_data'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL, data longtext NOT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id) ) $charset_collate;"; dbDelta($sql); } register_activation_hook(__FILE__, 'twhcp_light_activate_plugin'); // --- Plugin Deactivation --- function twhcp_light_deactivate_plugin() { // Add cleanup code here if needed. } register_deactivation_hook(__FILE__, 'twhcp_light_deactivate_plugin'); // --- Admin Menu --- function twhcp_light_add_3wart_menu() { add_menu_page( '3WART Content Performance Heatmap', '3WART Heatmap', 'manage_options', '3wart-content-heatmap-light', 'twhcp_light_admin_page_html', 'dashicons-chart-bar', 80 ); } add_action('admin_menu', 'twhcp_light_add_3wart_menu'); function twhcp_light_admin_page_html() { include_once(TWHCP_PLUGIN_DIR . 'includes/admin-page.php'); } // --- Admin Scripts --- function twhcp_light_admin_scripts($hook_suffix) { if ($hook_suffix !== 'toplevel_page_3wart-content-heatmap-light') return; wp_enqueue_style( 'twhcp-admin-styles', TWHCP_PLUGIN_URL . 'assets/css/3wart-cph-visualizer.css', [], TWHCP_VERSION ); wp_enqueue_script( 'twhcp-admin-visualizer', TWHCP_PLUGIN_URL . 'assets/js/3wart-cph-visualizer.js', ['jquery'], TWHCP_VERSION, true ); wp_localize_script( 'twhcp-admin-visualizer', 'twhcp_admin_ajax_object', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('twhcp_heatmap_admin_nonce'), ] ); } add_action('admin_enqueue_scripts', 'twhcp_light_admin_scripts'); // --- Front-End Tracker --- function twhcp_light_enqueue_tracker_script() { if (is_singular(['post', 'page'])) { wp_enqueue_script( 'twhcp-cph-tracker', TWHCP_PLUGIN_URL . 'assets/js/3wart-cph-tracker.js', ['jquery'], TWHCP_VERSION, true ); wp_localize_script( 'twhcp-cph-tracker', 'twhcp_ajax_object', [ 'ajax_url' => admin_url('admin-ajax.php'), 'post_id' => get_queried_object_id(), 'nonce' => wp_create_nonce('twhcp_heatmap_frontend_nonce'), 'user_status' => is_user_logged_in() ? 'loggedin' : 'guest', ] ); } } add_action('wp_enqueue_scripts', 'twhcp_light_enqueue_tracker_script'); // --- AJAX: Save Heatmap Data --- // Recursively sanitize an array or scalar function twhcp_light_recursive_sanitize( $data ) { if ( is_array( $data ) ) { return array_map( 'twhcp_light_recursive_sanitize', $data ); } // If it's numeric, preserve as number if ( is_numeric( $data ) ) { return $data + 0; // cast to int/float } return sanitize_text_field( $data ); } function twhcp_light_save_heatmap_data() { if ( ! isset( $_POST['security'] ) || ! check_ajax_referer( 'twhcp_heatmap_frontend_nonce', 'security', false ) ) { wp_send_json_error( 'Security check failed.' ); } if ( ! isset( $_POST['post_id'] ) || ! isset( $_POST['eventData'] ) ) { wp_send_json_error( 'Missing post ID or event data.' ); } $post_id = intval( $_POST['post_id'] ); // Remove slashes added by WordPress $raw_event_data = wp_unslash( $_POST['eventData'] ); // Decode JSON $decoded = json_decode( $raw_event_data, true ); if ( ! is_array( $decoded ) ) { wp_send_json_error( 'Invalid JSON payload.' ); } // ✅ Recursively sanitize the decoded array $decoded = twhcp_light_recursive_sanitize( $decoded ); // Sanitize session ID $session_id = sanitize_text_field( wp_unslash( $_POST['session_id'] ?? '' ) ); foreach ( $decoded as &$point ) { if ( is_array( $point ) ) { $point['session_id'] = $session_id; } } unset( $point ); global $wpdb; $table_name = $wpdb->prefix . 'twhcp_heatmap_data'; $result = $wpdb->insert( $table_name, [ 'post_id' => $post_id, 'data' => wp_json_encode( $decoded ), ], [ '%d', '%s' ] ); if ( false === $result ) { wp_send_json_error( 'Failed to save data.' ); } wp_send_json_success( 'Data saved successfully.' ); } add_action( 'wp_ajax_twhcp_light_save_heatmap_data', 'twhcp_light_save_heatmap_data' ); add_action( 'wp_ajax_nopriv_twhcp_light_save_heatmap_data', 'twhcp_light_save_heatmap_data' ); // --- AJAX: Get Heatmap Data --- function twhcp_light_get_heatmap_data() { if ( ! current_user_can( 'manage_options' ) || ! isset( $_POST['security'] ) || ! check_ajax_referer( 'twhcp_heatmap_admin_nonce', 'security', false ) ) { wp_send_json_error( 'You do not have permission to view this data.' ); } $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0; if ( ! $post_id || ! get_post_status( $post_id ) ) { wp_send_json_error( 'Invalid post ID.' ); } global $wpdb; $table_name = $wpdb->prefix . 'twhcp_heatmap_data'; $results = $wpdb->get_results( $wpdb->prepare("SELECT data FROM {$table_name} WHERE post_id = %d", $post_id) ); $heatmap_data = []; foreach ( $results as $result ) { $decoded = json_decode( $result->data, true ); if ( is_array( $decoded ) ) { $heatmap_data = array_merge( $heatmap_data, $decoded ); } } // Time filter with whitelist $allowed_time_filters = ['7', '30', 'custom']; $time_filter = isset($_POST['time_filter']) ? sanitize_text_field( wp_unslash( $_POST['time_filter'] ) ) : ''; if ( ! in_array( $time_filter, $allowed_time_filters, true ) ) { $time_filter = ''; } $start_date = isset($_POST['start_date']) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : ''; $end_date = isset($_POST['end_date']) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : ''; $from_ts = 0; $to_ts = time(); if ( $time_filter === '7' ) { $from_ts = strtotime('-7 days'); } elseif ( $time_filter === '30' ) { $from_ts = strtotime('-30 days'); } elseif ( $time_filter === 'custom' && $start_date && $end_date ) { $from_ts = strtotime( $start_date . ' 00:00:00' ); $to_ts = strtotime( $end_date . ' 23:59:59' ); } $filtered_data = array_filter( $heatmap_data, function( $point ) use ( $from_ts, $to_ts ) { if ( ! isset( $point['timestamp'] ) ) return false; $ts = intval( $point['timestamp'] ); if ( $ts > 9999999999 ) $ts = intval( $ts / 1000 ); return ( $ts >= $from_ts && $ts <= $to_ts ); }); // User segment filter $allowed_segments = ['all', 'loggedin', 'guest', 'first', 'returning']; $segment = isset($_POST['user_segment']) ? sanitize_text_field( wp_unslash( $_POST['user_segment'] ) ) : 'all'; if ( ! in_array( $segment, $allowed_segments, true ) ) { $segment = 'all'; } if ( $segment !== 'all' ) { $filtered_data = array_filter( $filtered_data, function( $point ) use ( $segment ) { return match($segment) { 'loggedin' => ($point['user_status'] ?? '') === 'loggedin', 'guest' => ($point['user_status'] ?? '') === 'guest', 'first' => ($point['visit_type'] ?? '') === 'first', 'returning' => ($point['visit_type'] ?? '') === 'returning', default => true, }; }); } // Session filter $session_id = isset($_POST['session_id']) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : ''; if ( $session_id && preg_match( '/^[a-zA-Z0-9_\-]{1,64}$/', $session_id ) ) { $filtered_data = array_filter( $filtered_data, fn($point) => ($point['session_id'] ?? '') === $session_id ); } // Sanitize before output $clean_output = array_map( 'twhcp_light_recursive_sanitize', $filtered_data ); wp_send_json_success( array_values( $clean_output ) ); } add_action('wp_ajax_twhcp_light_get_heatmap_data', 'twhcp_light_get_heatmap_data');