| 1 |
<?php |
| 2 |
|
| 3 |
namespace BPPIV\Analytics; |
| 4 |
|
| 5 |
if (!defined('ABSPATH')) { |
| 6 |
exit; |
| 7 |
} |
| 8 |
|
| 9 |
class AnalyticsManager { |
| 10 |
public function register() { |
| 11 |
// Initialize Database Tables |
| 12 |
add_action('admin_init', [$this, 'create_db_tables_if_needed']); |
| 13 |
|
| 14 |
// Register REST API Endpoints |
| 15 |
add_action('rest_api_init', [$this, 'register_rest_routes']); |
| 16 |
|
| 17 |
// Register Admin Menu |
| 18 |
add_action('admin_menu', [$this, 'register_analytics_admin_menu']); |
| 19 |
|
| 20 |
// Register CSV Export Handler |
| 21 |
add_action('admin_post_bppiv_export_analytics_csv', [$this, 'export_analytics_csv']); |
| 22 |
} |
| 23 |
|
| 24 |
public function create_db_tables_if_needed() { |
| 25 |
global $wpdb; |
| 26 |
$version_option = 'bppiv_analytics_db_version'; |
| 27 |
$current_version = get_option($version_option); |
| 28 |
|
| 29 |
if ($current_version === '1.0.0') { |
| 30 |
return; |
| 31 |
} |
| 32 |
|
| 33 |
require_once ABSPATH . 'wp-admin/includes/upgrade.php'; |
| 34 |
$charset_collate = $wpdb->get_charset_collate(); |
| 35 |
|
| 36 |
// 1. Raw Event Logs Table |
| 37 |
$logs_table = $wpdb->prefix . 'bppiv_analytics_logs'; |
| 38 |
$sql_logs = "CREATE TABLE {$logs_table} ( |
| 39 |
id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, |
| 40 |
product_id BIGINT(20) UNSIGNED DEFAULT 0, |
| 41 |
tour_id VARCHAR(100) DEFAULT '', |
| 42 |
event_type VARCHAR(30) NOT NULL, |
| 43 |
hotspot_id VARCHAR(100) DEFAULT '', |
| 44 |
hotspot_label VARCHAR(255) DEFAULT '', |
| 45 |
dwell_time INT(11) DEFAULT 0, |
| 46 |
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| 47 |
KEY product_id (product_id), |
| 48 |
KEY event_type (event_type), |
| 49 |
KEY created_at (created_at) |
| 50 |
) {$charset_collate};"; |
| 51 |
dbDelta($sql_logs); |
| 52 |
|
| 53 |
// 2. Daily Summary Table (Aggregated Stats) |
| 54 |
$summary_table = $wpdb->prefix . 'bppiv_analytics_summary'; |
| 55 |
$sql_summary = "CREATE TABLE {$summary_table} ( |
| 56 |
id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY, |
| 57 |
product_id BIGINT(20) UNSIGNED DEFAULT 0, |
| 58 |
event_type VARCHAR(30) NOT NULL, |
| 59 |
hotspot_id VARCHAR(100) DEFAULT '', |
| 60 |
hotspot_label VARCHAR(255) DEFAULT '', |
| 61 |
event_date DATE NOT NULL, |
| 62 |
total_count INT(11) DEFAULT 0, |
| 63 |
total_dwell_time INT(11) DEFAULT 0, |
| 64 |
UNIQUE KEY unique_summary (product_id, event_type, hotspot_id, event_date), |
| 65 |
KEY event_date (event_date) |
| 66 |
) {$charset_collate};"; |
| 67 |
dbDelta($sql_summary); |
| 68 |
|
| 69 |
update_option($version_option, '1.0.0'); |
| 70 |
} |
| 71 |
|
| 72 |
public function register_rest_routes() { |
| 73 |
register_rest_route('bppiv/v1', '/track-analytics', [ |
| 74 |
'methods' => 'POST', |
| 75 |
'callback' => [$this, 'handle_track_analytics'], |
| 76 |
'permission_callback' => '__return_true', |
| 77 |
]); |
| 78 |
} |
| 79 |
|
| 80 |
public function handle_track_analytics(\WP_REST_Request $request) { |
| 81 |
global $wpdb; |
| 82 |
|
| 83 |
$event_type = sanitize_key($request->get_param('event_type')); |
| 84 |
$product_id = intval($request->get_param('product_id')); |
| 85 |
$tour_id = sanitize_text_field($request->get_param('tour_id')); |
| 86 |
$hotspot_id = sanitize_text_field($request->get_param('hotspot_id')); |
| 87 |
$hotspot_label = sanitize_text_field($request->get_param('hotspot_label')); |
| 88 |
$dwell_time = intval($request->get_param('dwell_time')); |
| 89 |
|
| 90 |
// 1. Whitelist Valid Event Types |
| 91 |
$valid_events = ['impression', 'hotspot_click', 'cart_click', 'dwell_time']; |
| 92 |
if (!in_array($event_type, $valid_events, true)) { |
| 93 |
return new \WP_REST_Response(['success' => false, 'message' => 'Invalid event type'], 400); |
| 94 |
} |
| 95 |
|
| 96 |
// 2. Validate Product ID if provided |
| 97 |
if ($product_id > 0 && function_exists('wc_get_product')) { |
| 98 |
$product = wc_get_product($product_id); |
| 99 |
if (!$product) { |
| 100 |
return new \WP_REST_Response(['success' => false, 'message' => 'Invalid product ID'], 400); |
| 101 |
} |
| 102 |
} |
| 103 |
|
| 104 |
// 3. Insert Raw Log Event |
| 105 |
$table_name = $wpdb->prefix . 'bppiv_analytics_logs'; |
| 106 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 107 |
$inserted = $wpdb->insert( |
| 108 |
$table_name, |
| 109 |
[ |
| 110 |
'product_id' => $product_id, |
| 111 |
'tour_id' => $tour_id, |
| 112 |
'event_type' => $event_type, |
| 113 |
'hotspot_id' => $hotspot_id, |
| 114 |
'hotspot_label' => $hotspot_label, |
| 115 |
'dwell_time' => $dwell_time, |
| 116 |
'created_at' => current_time('mysql'), |
| 117 |
], |
| 118 |
['%d', '%s', '%s', '%s', '%s', '%d', '%s'] |
| 119 |
); |
| 120 |
|
| 121 |
if ($inserted) { |
| 122 |
return new \WP_REST_Response(['success' => true], 200); |
| 123 |
} |
| 124 |
|
| 125 |
return new \WP_REST_Response(['success' => false], 500); |
| 126 |
} |
| 127 |
|
| 128 |
public function register_analytics_admin_menu() { |
| 129 |
$page_hook = add_submenu_page( |
| 130 |
'edit.php?post_type=bppiv-image-viewer', |
| 131 |
esc_html__('Interaction Analytics', 'panorama'), |
| 132 |
'<span style="white-space:nowrap;">' . esc_html__('Analytics', 'panorama') . ' <span style="background:#146ef5;color:#fff;font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;text-transform:uppercase;line-height:1.2;display:inline-block;vertical-align:middle;margin-left:4px;">NEW</span></span>', |
| 133 |
'manage_options', |
| 134 |
'bppiv-analytics', |
| 135 |
[$this, 'render_analytics_admin_page'] |
| 136 |
); |
| 137 |
add_action('admin_print_styles-' . $page_hook, [$this, 'enqueue_analytics_assets']); |
| 138 |
} |
| 139 |
|
| 140 |
public function enqueue_analytics_assets() { |
| 141 |
wp_enqueue_style('bppiv-analytics-css', BPPIV_PLUGIN_DIR . 'inc/Analytics/assets/css/analytics.css', [], BPPIV_VERSION); |
| 142 |
} |
| 143 |
|
| 144 |
public function render_analytics_admin_page() { |
| 145 |
wp_enqueue_style('bppiv-analytics-css', BPPIV_PLUGIN_DIR . 'inc/Analytics/assets/css/analytics.css', [], BPPIV_VERSION); |
| 146 |
$template_file = BPPIV_PATH . 'inc/Analytics/templates/analytics-page.php'; |
| 147 |
if (file_exists($template_file)) { |
| 148 |
require_once $template_file; |
| 149 |
} else { |
| 150 |
echo '<div class="wrap"><h2>Analytics Template Not Found</h2></div>'; |
| 151 |
} |
| 152 |
} |
| 153 |
|
| 154 |
public function export_analytics_csv() { |
| 155 |
if (!current_user_can('manage_options')) { |
| 156 |
wp_die(esc_html__('Permission denied', 'panorama')); |
| 157 |
} |
| 158 |
|
| 159 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 160 |
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'bppiv_export_csv')) { |
| 161 |
wp_die(esc_html__('Security check failed', 'panorama')); |
| 162 |
} |
| 163 |
|
| 164 |
global $wpdb; |
| 165 |
$is_premium = function_exists('panoramaIsPremium') ? panoramaIsPremium() : false; |
| 166 |
$logs_table = $wpdb->prefix . 'bppiv_analytics_logs'; |
| 167 |
|
| 168 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 169 |
$range = isset($_GET['range']) ? sanitize_key($_GET['range']) : '7days'; |
| 170 |
$start_date = isset($_GET['start_date']) ? sanitize_text_field(wp_unslash($_GET['start_date'])) : ''; |
| 171 |
$end_date = isset($_GET['end_date']) ? sanitize_text_field(wp_unslash($_GET['end_date'])) : ''; |
| 172 |
|
| 173 |
if (!$is_premium) { |
| 174 |
$range = '7days'; |
| 175 |
} |
| 176 |
|
| 177 |
$where_clause = "WHERE 1=1"; |
| 178 |
|
| 179 |
if ($range === '7days') { |
| 180 |
$date_limit = gmdate('Y-m-d H:i:s', strtotime('-7 days')); |
| 181 |
$where_clause .= $wpdb->prepare(" AND created_at >= %s", $date_limit); |
| 182 |
} elseif ($range === '15days') { |
| 183 |
$date_limit = gmdate('Y-m-d H:i:s', strtotime('-15 days')); |
| 184 |
$where_clause .= $wpdb->prepare(" AND created_at >= %s", $date_limit); |
| 185 |
} elseif ($range === '30days') { |
| 186 |
$date_limit = gmdate('Y-m-d H:i:s', strtotime('-30 days')); |
| 187 |
$where_clause .= $wpdb->prepare(" AND created_at >= %s", $date_limit); |
| 188 |
} elseif ($range === 'custom' && !empty($start_date) && !empty($end_date)) { |
| 189 |
$start_datetime = $start_date . ' 00:00:00'; |
| 190 |
$end_datetime = $end_date . ' 23:59:59'; |
| 191 |
$where_clause .= $wpdb->prepare(" AND created_at >= %s AND created_at <= %s", $start_datetime, $end_datetime); |
| 192 |
} |
| 193 |
|
| 194 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 195 |
$logs = $wpdb->get_results(" |
| 196 |
SELECT id, event_type, hotspot_label, product_id, dwell_time, created_at |
| 197 |
FROM {$logs_table} |
| 198 |
{$where_clause} |
| 199 |
ORDER BY created_at DESC |
| 200 |
LIMIT 5000 |
| 201 |
"); |
| 202 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 203 |
|
| 204 |
$filename = 'panorama-analytics-report-' . gmdate('Y-m-d') . '.csv'; |
| 205 |
|
| 206 |
header('Content-Type: text/csv; charset=utf-8'); |
| 207 |
header('Content-Disposition: attachment; filename=' . $filename); |
| 208 |
|
| 209 |
// Writing directly to the php://output stream (not a real file), so |
| 210 |
// WP_Filesystem is not applicable here. |
| 211 |
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fputs, WordPress.WP.AlternativeFunctions.file_system_operations_fclose |
| 212 |
$output = fopen('php://output', 'w'); |
| 213 |
|
| 214 |
// Add UTF-8 BOM for Excel compatibility |
| 215 |
fputs($output, "\xEF\xBB\xBF"); |
| 216 |
|
| 217 |
// Header Row |
| 218 |
fputcsv($output, ['ID', 'Event Type', 'Hotspot / Label', 'Product Title / ID', 'Watch Duration (Sec)', 'Date & Time']); |
| 219 |
|
| 220 |
foreach ($logs as $log) { |
| 221 |
$event_name = ucfirst(str_replace('_', ' ', $log->event_type)); |
| 222 |
$label = $log->hotspot_label ? $log->hotspot_label : '360 Viewer'; |
| 223 |
$product = $log->product_id > 0 ? (get_the_title($log->product_id) ? get_the_title($log->product_id) : 'Product #' . $log->product_id) : 'N/A'; |
| 224 |
$dwell = $log->dwell_time > 0 ? $log->dwell_time : 'N/A'; |
| 225 |
|
| 226 |
fputcsv($output, [ |
| 227 |
$log->id, |
| 228 |
$event_name, |
| 229 |
$label, |
| 230 |
$product, |
| 231 |
$dwell, |
| 232 |
$log->created_at |
| 233 |
]); |
| 234 |
} |
| 235 |
|
| 236 |
fclose($output); |
| 237 |
// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fputs, WordPress.WP.AlternativeFunctions.file_system_operations_fclose |
| 238 |
exit; |
| 239 |
} |
| 240 |
} |
| 241 |
|