PluginProbe
Panorama – Turn Photos into Immersive Virtual Tours / trunk
Panorama – Turn Photos into Immersive Virtual Tours vtrunk
1.8.0 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.1 trunk 1.0.0 1.0.1 1.0.10 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.3 1.1.4 1.1.5 1.1.6 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 All 38 releases
panorama / inc / Analytics / AnalyticsManager.php

AnalyticsManager.php in Panorama – Turn Photos into Immersive Virtual Tours trunk, at inc/Analytics/AnalyticsManager.php

241 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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