PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Includes / Classes / Analytics / Data_Collector.php
embedpress / EmbedPress / Includes / Classes / Analytics Last commit date
Analytics_Manager.php 9 months ago Browser_Detector.php 10 months ago Content_Cache_Manager.php 10 months ago Data_Collector.php 3 weeks ago Email_Reports.php 10 months ago Export_Manager.php 10 months ago License_Manager.php 10 months ago Milestone_Manager.php 10 months ago Pro_Data_Collector.php 9 months ago REST_API.php 9 months ago
Data_Collector.php
3093 lines
1 <?php
2
3 namespace EmbedPress\Includes\Classes\Analytics;
4
5 use EmbedPress\Includes\Classes\Database\Analytics_Schema;
6 use EmbedPress\Includes\Classes\Analytics\License_Manager;
7
8 defined('ABSPATH') or die("No direct script access allowed.");
9
10 /**
11 * EmbedPress Analytics Data Collector
12 *
13 * Handles data collection and storage for analytics
14 *
15 * @package EmbedPress
16 * @author EmbedPress <help@embedpress.com>
17 * @copyright Copyright (C) 2023 WPDeveloper. All rights reserved.
18 * @license GPLv3 or later
19 * @since 4.2.7
20 */
21 class Data_Collector
22 {
23 private $license_manager;
24 private $pro_collector;
25
26 private static $view_count_cache = [];
27
28 private static $visitor_view_count_cache = [];
29
30 private static $download_count_cache = [];
31
32 public function __construct()
33 {
34 $this->license_manager = new License_Manager();
35 if ($this->license_manager->has_pro_license()) {
36 $this->pro_collector = new Pro_Data_Collector();
37 }
38 }
39
40 /**
41 * Public read helper: total recorded views for a given content_id.
42 * Memoized per-request; safe to call multiple times during a page render.
43 */
44 public function get_view_count_by_content_id($content_id)
45 {
46 $content_id = sanitize_text_field((string) $content_id);
47 if ($content_id === '') {
48 return 0;
49 }
50 if (isset(self::$view_count_cache[$content_id])) {
51 return self::$view_count_cache[$content_id];
52 }
53
54 global $wpdb;
55 $table = $wpdb->prefix . 'embedpress_analytics_views';
56 $count = (int) $wpdb->get_var($wpdb->prepare(
57 "SELECT COUNT(*) FROM $table WHERE content_id = %s AND interaction_type = %s",
58 $content_id,
59 'view'
60 ));
61
62 self::$view_count_cache[$content_id] = $count;
63 return $count;
64 }
65
66 /**
67 * Public read helper: view count for the VISITOR-FACING badge only.
68 *
69 * The badge writes its own rows tagged `interaction_data.source =
70 * 'visitor_view_count'` (one per page-load, per embed instance). The
71 * analytics tracker ALSO writes `interaction_type='view'` rows for the
72 * same content_id and re-fires them on scroll / viewport re-entry — so
73 * the unfiltered get_view_count_by_content_id() would let those tracker
74 * rows inflate the badge as the visitor scrolls. Counting only the
75 * badge's own rows keeps the badge at one-view-per-page-load regardless
76 * of whether the dashboard tracker is active. Memoized per-request.
77 */
78 public function get_visitor_view_count_by_content_id($content_id)
79 {
80 $content_id = sanitize_text_field((string) $content_id);
81 if ($content_id === '') {
82 return 0;
83 }
84 if (isset(self::$visitor_view_count_cache[$content_id])) {
85 return self::$visitor_view_count_cache[$content_id];
86 }
87
88 global $wpdb;
89 $table = $wpdb->prefix . 'embedpress_analytics_views';
90 $count = (int) $wpdb->get_var($wpdb->prepare(
91 "SELECT COUNT(*) FROM $table
92 WHERE content_id = %s
93 AND interaction_type = %s
94 AND JSON_UNQUOTE(JSON_EXTRACT(interaction_data, '$.source')) = %s",
95 $content_id,
96 'view',
97 'visitor_view_count'
98 ));
99
100 self::$visitor_view_count_cache[$content_id] = $count;
101 return $count;
102 }
103
104 /**
105 * Public read helper: total recorded downloads for a given content_id.
106 *
107 * Downloads share the analytics views table (no schema migration) and are
108 * stored as `interaction_type='click'` rows whose `interaction_data` JSON
109 * carries `source = "download"`. Memoized per-request.
110 */
111 public function get_download_count_by_content_id($content_id)
112 {
113 $content_id = sanitize_text_field((string) $content_id);
114 if ($content_id === '') {
115 return 0;
116 }
117 if (isset(self::$download_count_cache[$content_id])) {
118 return self::$download_count_cache[$content_id];
119 }
120
121 global $wpdb;
122 $table = $wpdb->prefix . 'embedpress_analytics_views';
123 $count = (int) $wpdb->get_var($wpdb->prepare(
124 "SELECT COUNT(*) FROM $table
125 WHERE content_id = %s
126 AND interaction_type = %s
127 AND JSON_UNQUOTE(JSON_EXTRACT(interaction_data, '$.source')) = %s",
128 $content_id,
129 'click',
130 'download'
131 ));
132
133 self::$download_count_cache[$content_id] = $count;
134 return $count;
135 }
136
137 /**
138 * Build date condition for SQL queries
139 *
140 * @param array $args
141 * @param string $date_column
142 * @return string
143 */
144 private function build_date_condition($args = [], $date_column = 'created_at')
145 {
146 global $wpdb;
147
148 $date_condition = '';
149
150 // Check if specific start_date and end_date are provided
151 if (!empty($args['start_date']) && !empty($args['end_date'])) {
152 $start_date = sanitize_text_field($args['start_date']);
153 $end_date = sanitize_text_field($args['end_date']);
154
155 // Validate date format (YYYY-MM-DD)
156 if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
157 $date_condition = $wpdb->prepare(
158 "AND DATE($date_column) BETWEEN %s AND %s",
159 $start_date,
160 $end_date
161 );
162 }
163 } else {
164 // Fall back to date_range (number of days)
165 $date_range = isset($args['date_range']) ? absint($args['date_range']) : 30;
166
167 if ($date_range > 0) {
168 $date_condition = $wpdb->prepare(
169 "AND $date_column >= DATE_SUB(NOW(), INTERVAL %d DAY)",
170 $date_range
171 );
172 }
173 }
174 return $date_condition;
175 }
176
177 /**
178 * Track content creation
179 *
180 * @param string $content_id
181 * @param string $content_type
182 * @param array $data
183 * @return bool
184 */
185 public function track_content_creation($content_id, $content_type, $data = [])
186 {
187 global $wpdb;
188
189 $table_name = $wpdb->prefix . 'embedpress_analytics_content';
190
191 $insert_data = [
192 'content_id' => sanitize_text_field($content_id),
193 'content_type' => sanitize_text_field($content_type),
194 'embed_type' => isset($data['embed_type']) ? sanitize_text_field($data['embed_type']) : '',
195 'embed_url' => isset($data['embed_url']) ? esc_url_raw($data['embed_url']) : '',
196 'post_id' => isset($data['post_id']) ? absint($data['post_id']) : get_the_ID(),
197 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : get_permalink(),
198 'title' => isset($data['title']) ? sanitize_text_field($data['title']) : get_the_title(),
199 'created_at' => current_time('mysql'),
200 'updated_at' => current_time('mysql')
201 ];
202
203 // Use INSERT ... ON DUPLICATE KEY UPDATE to handle existing content
204 $sql = "INSERT INTO $table_name (content_id, content_type, embed_type, embed_url, post_id, page_url, title, created_at, updated_at)
205 VALUES (%s, %s, %s, %s, %d, %s, %s, %s, %s)
206 ON DUPLICATE KEY UPDATE
207 embed_type = VALUES(embed_type),
208 embed_url = VALUES(embed_url),
209 post_id = VALUES(post_id),
210 page_url = VALUES(page_url),
211 title = VALUES(title),
212 updated_at = VALUES(updated_at)";
213
214 $result = $wpdb->query($wpdb->prepare(
215 $sql,
216 $insert_data['content_id'],
217 $insert_data['content_type'],
218 $insert_data['embed_type'],
219 $insert_data['embed_url'],
220 $insert_data['post_id'],
221 $insert_data['page_url'],
222 $insert_data['title'],
223 $insert_data['created_at'],
224 $insert_data['updated_at']
225 ));
226
227 return $result !== false;
228 }
229
230 /**
231 * Track interaction (view, click, impression, etc.)
232 * OPTIMIZED: One row per user per content, update counters instead of creating multiple rows
233 *
234 * @param array $data
235 * @return bool
236 */
237 public function track_interaction($data)
238 {
239 global $wpdb;
240
241 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
242 $content_id = sanitize_text_field($data['content_id']);
243 $interaction_type = sanitize_text_field($data['interaction_type']);
244 $session_id = sanitize_text_field($data['session_id']);
245
246 // Get user identifier - prefer user_id from localStorage, fallback to session
247 $user_identifier = isset($data['user_id']) && !empty($data['user_id']) && $data['user_id'] !== 'null'
248 ? sanitize_text_field($data['user_id'])
249 : $session_id;
250
251 // Track referrer analytics for external referrers
252 $this->track_referrer_from_interaction_data($data, $interaction_type, $user_identifier);
253
254 // Look for existing record for this user+content combination
255 // Use session_id field to store our user_identifier for backwards compatibility
256 $existing_record = $wpdb->get_row($wpdb->prepare(
257 "SELECT * FROM $views_table
258 WHERE session_id = %s AND content_id = %s
259 ORDER BY created_at DESC LIMIT 1",
260 $user_identifier,
261 $content_id
262 ));
263
264
265 if ($existing_record) {
266 // Update existing record - increment counters instead of creating new rows
267 return $this->update_interaction_counters($existing_record, $interaction_type, $data);
268
269 } else {
270 // Create new record for this user+content combination
271 return $this->create_optimized_interaction_record($data, $user_identifier);
272 }
273 }
274
275 /**
276 * Create optimized interaction record with counters
277 */
278 private function create_optimized_interaction_record($data, $user_identifier)
279 {
280 global $wpdb;
281
282 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
283 $content_id = sanitize_text_field($data['content_id']);
284 $interaction_type = sanitize_text_field($data['interaction_type']);
285
286 // Get referrer URL - use the original referrer captured on first server request
287 $referrer_url = '';
288
289 // Priority 1: Use the original referrer captured in main plugin file
290 if (defined('EMBEDPRESS_ORIGINAL_REFERRER') && !empty(EMBEDPRESS_ORIGINAL_REFERRER)) {
291 $referrer_url = esc_url_raw(EMBEDPRESS_ORIGINAL_REFERRER);
292 }
293
294
295 // Priority 2 (now client-side referrer from JavaScript)
296 if (empty($referrer_url) && isset($data['original_referrer']) && !empty($data['original_referrer'])) {
297 $current_site_url = home_url();
298 $client_referrer = esc_url_raw($data['original_referrer']);
299 // Only use if it's external
300 if (strpos($client_referrer, $current_site_url) !== 0) {
301 $referrer_url = $client_referrer;
302 }
303 }
304
305 // Initialize counters based on interaction type
306 $interaction_data = [
307 'content_id' => $content_id,
308 'user_identifier' => $user_identifier,
309 'user_ip' => $this->get_user_ip(),
310 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
311 'referrer_url' => $referrer_url,
312 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
313 'interaction_data' => isset($data['interaction_data']) ? wp_json_encode($data['interaction_data']) : null,
314 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
315 'created_at' => current_time('mysql'),
316 // Initialize counters
317 'view_count' => $interaction_type === 'view' ? 1 : 0,
318 'click_count' => $interaction_type === 'click' ? 1 : 0,
319 'impression_count' => $interaction_type === 'impression' ? 1 : 0,
320 'first_' . $interaction_type . '_at' => current_time('mysql'),
321 'last_' . $interaction_type . '_at' => current_time('mysql')
322 ];
323
324 $result = $wpdb->insert($views_table, [
325 'content_id' => $content_id,
326 'session_id' => $user_identifier, // Store user_identifier in session_id field
327 'interaction_type' => 'combined', // Mark as combined record
328 'interaction_data' => wp_json_encode($interaction_data),
329 'user_ip' => $this->get_user_ip(),
330 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
331 'referrer_url' => $referrer_url,
332 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
333 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
334 'created_at' => current_time('mysql')
335 ]);
336
337 if ($result) {
338 // Update content table counters
339 $this->update_content_counters($content_id, $interaction_type, $data['interaction_data'] ?? [], $data['page_url'] ?? '');
340 }
341
342 return $result !== false;
343 }
344
345
346 /**
347 * Update interaction counters for existing record
348 */
349 private function update_interaction_counters($existing_record, $interaction_type, $data)
350 {
351 global $wpdb;
352
353 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
354
355 // Parse existing interaction data
356 $interaction_data = json_decode($existing_record->interaction_data, true) ?: [];
357
358 // Initialize counters if they don't exist
359 if (!isset($interaction_data['view_count'])) $interaction_data['view_count'] = 0;
360 if (!isset($interaction_data['click_count'])) $interaction_data['click_count'] = 0;
361 if (!isset($interaction_data['impression_count'])) $interaction_data['impression_count'] = 0;
362
363 // Update counter based on interaction type
364 switch ($interaction_type) {
365 case 'view':
366 $interaction_data['view_count']++;
367 $interaction_data['last_view_at'] = current_time('mysql');
368 break;
369 case 'click':
370 $interaction_data['click_count']++;
371 $interaction_data['last_click_at'] = current_time('mysql');
372 break;
373 case 'impression':
374 $interaction_data['impression_count']++;
375 $interaction_data['last_impression_at'] = current_time('mysql');
376 break;
377 }
378
379 // Update view duration if provided
380 if (isset($data['view_duration']) && $data['view_duration'] > 0) {
381 $interaction_data['total_view_duration'] = ($interaction_data['total_view_duration'] ?? 0) + absint($data['view_duration']);
382 }
383
384 // Update the record
385 $result = $wpdb->update(
386 $views_table,
387 [
388 'interaction_data' => wp_json_encode($interaction_data),
389 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : $existing_record->view_duration
390 ],
391 ['id' => $existing_record->id]
392 );
393
394 if ($result !== false) {
395 // Update content table counters
396 $this->update_content_counters($existing_record->content_id, $interaction_type, $data['interaction_data'] ?? [], $data['page_url'] ?? '');
397 }
398
399 return $result !== false;
400 }
401
402 /**
403 * Create new interaction record
404 */
405 private function create_new_interaction_record($data)
406 {
407 global $wpdb;
408
409 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
410 $content_id = sanitize_text_field($data['content_id']);
411 $interaction_type = sanitize_text_field($data['interaction_type']);
412
413 // Get user identifier - prefer user_id from localStorage, fallback to session
414 $user_id = isset($data['user_id']) && !empty($data['user_id']) && $data['user_id'] !== 'null'
415 ? sanitize_text_field($data['user_id'])
416 : null;
417
418 // Get referrer URL from HTTP_REFERER only
419 $referrer_url = '';
420 if (!empty($_SERVER['HTTP_REFERER'])) {
421 $current_site_url = home_url();
422 $http_referrer = esc_url_raw($_SERVER['HTTP_REFERER']);
423 // Only capture if it's from external source
424 if (strpos($http_referrer, $current_site_url) !== 0) {
425 $referrer_url = $http_referrer;
426 }
427 }
428
429 $interaction_data = [
430 'content_id' => $content_id,
431 'user_id' => $user_id,
432 'session_id' => sanitize_text_field($data['session_id']),
433 'user_ip' => $this->get_user_ip(),
434 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
435 'referrer_url' => $referrer_url,
436 'page_url' => isset($data['page_url']) ? esc_url_raw($data['page_url']) : '',
437 'interaction_type' => $interaction_type,
438 'interaction_data' => isset($data['interaction_data']) ? wp_json_encode($data['interaction_data']) : null,
439 'view_duration' => isset($data['view_duration']) ? absint($data['view_duration']) : 0,
440 'created_at' => current_time('mysql')
441 ];
442
443 $result = $wpdb->insert($views_table, $interaction_data);
444
445 if ($result) {
446 // Update content table counters with additional data
447 $interaction_data = isset($data['interaction_data']) ? $data['interaction_data'] : [];
448 // If interaction_data is a JSON string, decode it; otherwise use as-is
449 if (is_string($interaction_data)) {
450 $interaction_data = json_decode($interaction_data, true) ?: [];
451 }
452 $page_url = isset($data['page_url']) ? $data['page_url'] : '';
453 $this->update_content_counters($data['content_id'], $data['interaction_type'], $interaction_data, $page_url);
454
455 // Browser info is now stored from frontend via REST API
456 // $this->store_browser_info($data['session_id']);
457 }
458
459 return $result !== false;
460 }
461
462 /**
463 * Update content counters
464 *
465 * @param string $content_id
466 * @param string $interaction_type
467 * @param array $interaction_data Additional data from the interaction
468 * @param string $page_url The page URL where the interaction occurred
469 * @return void
470 */
471 private function update_content_counters($content_id, $interaction_type, $interaction_data = [], $page_url = '')
472 {
473 global $wpdb;
474
475 $table_name = $wpdb->prefix . 'embedpress_analytics_content';
476
477 $counter_field = '';
478 switch ($interaction_type) {
479 case 'view':
480 $counter_field = 'total_views';
481 break;
482 case 'click':
483 $counter_field = 'total_clicks';
484 break;
485 case 'impression':
486 $counter_field = 'total_impressions';
487 break;
488 default:
489 return;
490 }
491
492 // Extract content information first to get embed_type and page_url
493 $content_info = $this->extract_content_info($content_id, $interaction_data, $page_url);
494
495 // Skip tracking if embed type cannot be determined
496 if ($content_info === null) {
497 return;
498 }
499
500 // Keep original embed_type name without transformation
501 $embed_type = $content_info['embed_type'];
502
503 // Check if content record exists based on page_url + embed_type (not content_id)
504 $content_exists = $wpdb->get_var($wpdb->prepare(
505 "SELECT id FROM $table_name WHERE page_url = %s AND embed_type = %s",
506 $page_url,
507 $embed_type
508 ));
509
510 if ($content_exists) {
511 // Update existing record
512 $sql = "UPDATE $table_name SET $counter_field = $counter_field + 1, updated_at = %s WHERE page_url = %s AND embed_type = %s";
513 $wpdb->query($wpdb->prepare($sql, current_time('mysql'), $page_url, $embed_type));
514 } else {
515 // Create new record with the counter set to 1 (content_info already extracted above)
516 $insert_data = [
517 'content_id' => $content_id,
518 'content_type' => $content_info['content_type'],
519 'embed_type' => $embed_type, // Use original embed_type without transformation
520 'embed_url' => $content_info['embed_url'],
521 'post_id' => $content_info['post_id'],
522 'page_url' => $page_url,
523 'title' => $content_info['title'],
524 'total_views' => $interaction_type === 'view' ? 1 : 0,
525 'total_clicks' => $interaction_type === 'click' ? 1 : 0,
526 'total_impressions' => $interaction_type === 'impression' ? 1 : 0,
527 'created_at' => current_time('mysql'),
528 'updated_at' => current_time('mysql')
529 ];
530
531 $wpdb->insert($table_name, $insert_data);
532 }
533 }
534
535 /**
536 * Extract content information from content ID and interaction data
537 *
538 * @param string $content_id
539 * @param array $interaction_data
540 * @param string $page_url
541 * @return array|null Returns null if embed_type cannot be determined (to skip tracking)
542 */
543 private function extract_content_info($content_id, $interaction_data = [], $page_url = '')
544 {
545
546
547 // Default values
548 $content_info = [
549 'content_type' => $this->detect_content_type($page_url, $interaction_data), // This represents how content was embedded (elementor/gutenberg/shortcode)
550 'embed_type' => 'unknown', // This is the source type (youtube, vimeo, pdf, etc.)
551 'embed_url' => '',
552 'post_id' => null,
553 'title' => 'Unknown Page' // This should be the page title, not content title
554 ];
555
556 // Extract embed type (source type) from interaction data (most reliable)
557 if (!empty($interaction_data['embed_type']) && $interaction_data['embed_type'] !== 'unknown') {
558 $content_info['embed_type'] = sanitize_text_field($interaction_data['embed_type']); // Keep original case
559 }
560
561 // Extract embed URL from interaction data
562 if (!empty($interaction_data['embed_url'])) {
563 $content_info['embed_url'] = esc_url_raw($interaction_data['embed_url']);
564 }
565
566 // The title should be the page title, not the embed title
567 // We'll get this from the page_url or post_id
568
569 // Extract information from content ID patterns if embed_type is still unknown
570 if ($content_info['embed_type'] === 'unknown') {
571 $content_id_info = $this->analyze_content_id($content_id);
572 if ($content_id_info['embed_type'] !== 'unknown') {
573 $content_info['embed_type'] = $content_id_info['embed_type']; // Keep original case
574 }
575 if (!empty($content_id_info['embed_url'])) {
576 $content_info['embed_url'] = $content_id_info['embed_url'];
577 }
578 }
579
580 // Try to detect from embed URL if still unknown
581 if ($content_info['embed_type'] === 'unknown' && !empty($content_info['embed_url'])) {
582 $url_detected_type = $this->detect_embed_type_from_url($content_info['embed_url']);
583 if ($url_detected_type !== 'unknown') {
584 $content_info['embed_type'] = $url_detected_type;
585 }
586 }
587
588 // If we still can't determine the embed type, return null to skip tracking
589 if ($content_info['embed_type'] === 'unknown') {
590 return null;
591 }
592
593 // Try to extract post ID from page URL
594 if (!empty($page_url)) {
595 $content_info['post_id'] = $this->extract_post_id_from_url($page_url);
596 }
597
598 // Get the page title (this is what should be displayed as "Content")
599 $content_info['title'] = $this->get_page_title($content_info['post_id'], $page_url);
600
601 return $content_info;
602 }
603
604 /**
605 * Detect content type (platform) based on page URL and interaction data
606 *
607 * @param string $page_url
608 * @param array $interaction_data
609 * @return string
610 */
611 private function detect_content_type($page_url = '', $interaction_data = [])
612 {
613 // Check interaction data for platform hints (most reliable)
614 if (!empty($interaction_data['platform']) && $interaction_data['platform'] !== 'unknown') {
615 return sanitize_text_field($interaction_data['platform']);
616 }
617
618 // Try to detect from page content if we have a post ID
619 if (!empty($page_url)) {
620 $post_id = $this->extract_post_id_from_url($page_url);
621 if ($post_id) {
622 // Check for Elementor first (most specific)
623 if (
624 get_post_meta($post_id, '_elementor_edit_mode', true) ||
625 get_post_meta($post_id, '_elementor_data', true)
626 ) {
627 return 'elementor';
628 }
629
630 $post_content = get_post_field('post_content', $post_id);
631 if ($post_content) {
632 // Check for Elementor in content
633 if (
634 strpos($post_content, 'elementor') !== false ||
635 strpos($post_content, 'data-widget_type') !== false ||
636 strpos($post_content, 'data-element_type') !== false
637 ) {
638 return 'elementor';
639 }
640
641 // Check for Gutenberg blocks (more specific patterns)
642 if (
643 strpos($post_content, '<!-- wp:embedpress/') !== false ||
644 strpos($post_content, 'wp:embedpress/') !== false ||
645 (strpos($post_content, '<!-- wp:') !== false && strpos($post_content, 'embedpress') !== false)
646 ) {
647 return 'gutenberg';
648 }
649
650 // Check for shortcodes
651 if (
652 strpos($post_content, '[embedpress') !== false ||
653 strpos($post_content, '[ep-') !== false
654 ) {
655 return 'shortcode';
656 }
657
658 // Additional Gutenberg check for any wp: blocks
659 if (strpos($post_content, '<!-- wp:') !== false) {
660 return 'gutenberg';
661 }
662 }
663 }
664 }
665
666 // Default fallback if no platform can be detected
667 return 'unknown';
668 }
669
670 /**
671 * Map embed type to content type
672 *
673 * @param string $embed_type
674 * @return string
675 */
676 private function map_embed_type_to_content_type($embed_type)
677 {
678 $type_mapping = [
679 'youtube' => 'video',
680 'vimeo' => 'video',
681 'dailymotion' => 'video',
682 'twitch' => 'video',
683 'wistia' => 'video',
684 'pdf' => 'document',
685 'document' => 'document',
686 'google-docs' => 'document',
687 'google-sheets' => 'document',
688 'google-slides' => 'presentation',
689 'google-forms' => 'form',
690 'google-drawings' => 'image',
691 'google-maps' => 'map',
692 'soundcloud' => 'audio',
693 'spotify' => 'audio',
694 'facebook' => 'social',
695 'twitter' => 'social',
696 'instagram' => 'social',
697 'embedpress' => 'embed'
698 ];
699
700 return isset($type_mapping[$embed_type]) ? $type_mapping[$embed_type] : 'unknown';
701 }
702
703 /**
704 * Analyze content ID to extract embed information
705 *
706 * @param string $content_id
707 * @return array
708 */
709 private function analyze_content_id($content_id)
710 {
711 $info = [
712 'embed_type' => 'unknown',
713 'embed_url' => ''
714 ];
715
716 // Check for specific patterns in content ID to determine source type
717 // Core video providers
718 if (strpos($content_id, 'youtube') !== false) {
719 $info['embed_type'] = 'youtube';
720 } elseif (strpos($content_id, 'vimeo') !== false) {
721 $info['embed_type'] = 'vimeo';
722 } elseif (strpos($content_id, 'wistia') !== false) {
723 $info['embed_type'] = 'wistia';
724 } elseif (strpos($content_id, 'twitch') !== false) {
725 $info['embed_type'] = 'twitch';
726
727 // Document providers
728 } elseif (strpos($content_id, 'pdf') !== false || strpos($content_id, 'embedpress-pdf') !== false) {
729 $info['embed_type'] = 'pdf';
730 } elseif (strpos($content_id, 'google-docs') !== false) {
731 $info['embed_type'] = 'google-docs';
732 } elseif (strpos($content_id, 'google-sheets') !== false) {
733 $info['embed_type'] = 'google-sheets';
734 } elseif (strpos($content_id, 'google-slides') !== false) {
735 $info['embed_type'] = 'google-slides';
736 } elseif (strpos($content_id, 'google-forms') !== false) {
737 $info['embed_type'] = 'google-forms';
738 } elseif (strpos($content_id, 'google-drive') !== false) {
739 $info['embed_type'] = 'google-drive';
740
741 // Google services
742 } elseif (strpos($content_id, 'google-maps') !== false) {
743 $info['embed_type'] = 'google-maps';
744 } elseif (strpos($content_id, 'google-photos') !== false) {
745 $info['embed_type'] = 'google-photos';
746
747 // Social media providers
748 } elseif (strpos($content_id, 'instagram') !== false) {
749 $info['embed_type'] = 'instagram';
750 } elseif (strpos($content_id, 'twitter') !== false || strpos($content_id, 'x.com') !== false) {
751 $info['embed_type'] = 'twitter';
752 } elseif (strpos($content_id, 'linkedin') !== false) {
753 $info['embed_type'] = 'linkedin';
754
755 // Media and entertainment
756 } elseif (strpos($content_id, 'giphy') !== false) {
757 $info['embed_type'] = 'giphy';
758 } elseif (strpos($content_id, 'boomplay') !== false) {
759 $info['embed_type'] = 'boomplay';
760 } elseif (strpos($content_id, 'spreaker') !== false) {
761 $info['embed_type'] = 'spreaker';
762 } elseif (strpos($content_id, 'nrk') !== false) {
763 $info['embed_type'] = 'nrk-radio';
764
765 // Business and productivity
766 } elseif (strpos($content_id, 'calendly') !== false) {
767 $info['embed_type'] = 'calendly';
768 } elseif (strpos($content_id, 'airtable') !== false) {
769 $info['embed_type'] = 'airtable';
770 } elseif (strpos($content_id, 'canva') !== false) {
771 $info['embed_type'] = 'canva';
772
773 // E-commerce and marketplaces
774 } elseif (strpos($content_id, 'opensea') !== false) {
775 $info['embed_type'] = 'opensea';
776 } elseif (strpos($content_id, 'gumroad') !== false) {
777 $info['embed_type'] = 'gumroad';
778
779 // Development
780 } elseif (strpos($content_id, 'github') !== false) {
781 $info['embed_type'] = 'github';
782
783 // Generic EmbedPress content
784 } elseif (strpos($content_id, 'source-') !== false) {
785 $info['embed_type'] = 'embedpress';
786 }
787
788 return $info;
789 }
790
791 /**
792 * Detect embed type from URL patterns
793 *
794 * @param string $url
795 * @return string
796 */
797 private function detect_embed_type_from_url($url)
798 {
799 if (empty($url)) {
800 return 'unknown';
801 }
802
803 // Normalize URL for pattern matching
804 $url = strtolower($url);
805
806 // Video providers
807 if (strpos($url, 'youtube.com') !== false || strpos($url, 'youtu.be') !== false) {
808 return 'youtube';
809 } elseif (strpos($url, 'vimeo.com') !== false) {
810 return 'vimeo';
811 } elseif (strpos($url, 'wistia.com') !== false) {
812 return 'wistia';
813 } elseif (strpos($url, 'twitch.tv') !== false) {
814 return 'twitch';
815
816 // Google services
817 } elseif (strpos($url, 'docs.google.com') !== false) {
818 return 'google-docs';
819 } elseif (strpos($url, 'sheets.google.com') !== false) {
820 return 'google-sheets';
821 } elseif (strpos($url, 'slides.google.com') !== false) {
822 return 'google-slides';
823 } elseif (strpos($url, 'forms.google.com') !== false) {
824 return 'google-forms';
825 } elseif (strpos($url, 'drive.google.com') !== false) {
826 return 'google-drive';
827 } elseif (strpos($url, 'maps.google.com') !== false || strpos($url, 'goo.gl') !== false) {
828 return 'google-maps';
829 } elseif (strpos($url, 'photos.google.com') !== false || strpos($url, 'photos.app.goo.gl') !== false) {
830 return 'google-photos';
831
832 // Social media
833 } elseif (strpos($url, 'instagram.com') !== false) {
834 return 'instagram';
835 } elseif (strpos($url, 'twitter.com') !== false || strpos($url, 'x.com') !== false) {
836 return 'twitter';
837 } elseif (strpos($url, 'linkedin.com') !== false) {
838 return 'linkedin';
839
840 // Media and entertainment
841 } elseif (strpos($url, 'giphy.com') !== false) {
842 return 'giphy';
843 } elseif (strpos($url, 'boomplay.com') !== false) {
844 return 'boomplay';
845 } elseif (strpos($url, 'spreaker.com') !== false) {
846 return 'spreaker';
847 } elseif (strpos($url, 'radio.nrk.no') !== false || strpos($url, 'nrk.no') !== false) {
848 return 'nrk-radio';
849
850 // Business and productivity
851 } elseif (strpos($url, 'calendly.com') !== false) {
852 return 'calendly';
853 } elseif (strpos($url, 'airtable.com') !== false) {
854 return 'airtable';
855 } elseif (strpos($url, 'canva.com') !== false) {
856 return 'canva';
857
858 // E-commerce and marketplaces
859 } elseif (strpos($url, 'opensea.io') !== false) {
860 return 'opensea';
861 } elseif (strpos($url, 'gumroad.com') !== false) {
862 return 'gumroad';
863
864 // Development
865 } elseif (strpos($url, 'github.com') !== false || strpos($url, 'gist.github.com') !== false) {
866 return 'github';
867
868 // PDF files
869 } elseif (strpos($url, '.pdf') !== false) {
870 return 'pdf';
871 }
872
873 return 'unknown';
874 }
875
876 /**
877 * Get page title from post ID or URL
878 *
879 * @param int|null $post_id
880 * @param string $page_url
881 * @return string
882 */
883 private function get_page_title($post_id = null, $page_url = '')
884 {
885 // Try to get title from post ID first
886 if ($post_id) {
887 $title = get_the_title($post_id);
888 if (!empty($title)) {
889 return sanitize_text_field($title);
890 }
891 }
892
893 // Try to extract from URL
894 if (!empty($page_url)) {
895 // Try to get post ID from URL if we don't have it
896 if (!$post_id) {
897 $post_id = url_to_postid($page_url);
898 if ($post_id) {
899 $title = get_the_title($post_id);
900 if (!empty($title)) {
901 return sanitize_text_field($title);
902 }
903 }
904 }
905
906 // Fallback: extract page name from URL
907 $parsed_url = parse_url($page_url);
908 if (!empty($parsed_url['path'])) {
909 $path_parts = explode('/', trim($parsed_url['path'], '/'));
910 $page_slug = end($path_parts);
911 if (!empty($page_slug)) {
912 return ucwords(str_replace(['-', '_'], ' ', $page_slug));
913 }
914 }
915 }
916
917 return 'Unknown Page';
918 }
919
920 /**
921 * Generate a meaningful title for content
922 *
923 * @param string $embed_type
924 * @param string $content_id
925 * @param string $page_url
926 * @return string
927 */
928 private function generate_content_title($embed_type, $content_id, $page_url = '')
929 {
930 // Create meaningful titles based on embed type
931 $type_titles = [
932 'youtube' => 'YouTube Video',
933 'vimeo' => 'Vimeo Video',
934 'dailymotion' => 'Dailymotion Video',
935 'twitch' => 'Twitch Stream',
936 'wistia' => 'Wistia Video',
937 'pdf' => 'PDF Document',
938 'document' => 'Document',
939 'google-docs' => 'Google Docs',
940 'google-sheets' => 'Google Sheets',
941 'google-slides' => 'Google Slides',
942 'google-forms' => 'Google Forms',
943 'google-drawings' => 'Google Drawings',
944 'google-maps' => 'Google Maps',
945 'soundcloud' => 'SoundCloud Audio',
946 'spotify' => 'Spotify Audio',
947 'facebook' => 'Facebook Post',
948 'twitter' => 'Twitter Post',
949 'instagram' => 'Instagram Post',
950 'embedpress' => 'EmbedPress Content'
951 ];
952
953 $base_title = isset($type_titles[$embed_type]) ? $type_titles[$embed_type] : 'Embedded Content';
954
955 // Try to get more specific info from content ID
956 if (strpos($content_id, 'auto-') === 0) {
957 // Auto-generated ID, try to extract from page context
958 if (!empty($page_url)) {
959 $post_id = $this->extract_post_id_from_url($page_url);
960 if ($post_id) {
961 $post_title = get_the_title($post_id);
962 if (!empty($post_title)) {
963 return $base_title . ' in "' . $post_title . '"';
964 }
965 }
966 }
967 } else {
968 // Use content ID as part of title for identification
969 $short_id = substr($content_id, 0, 8);
970 return $base_title . ' (' . $short_id . ')';
971 }
972
973 return $base_title;
974 }
975
976 /**
977 * Extract post ID from URL
978 *
979 * @param string $url
980 * @return int|null
981 */
982 private function extract_post_id_from_url($url)
983 {
984 // Try to get post ID from URL
985 $post_id = url_to_postid($url);
986 if ($post_id) {
987 return $post_id;
988 }
989
990 // Fallback: try to extract from URL patterns
991 if (preg_match('/[?&]p=(\d+)/', $url, $matches)) {
992 return intval($matches[1]);
993 }
994
995 if (preg_match('/[?&]page_id=(\d+)/', $url, $matches)) {
996 return intval($matches[1]);
997 }
998
999 return null;
1000 }
1001
1002 /**
1003 * Store browser information
1004 *
1005 * @param string $session_id
1006 * @return void
1007 */
1008 private function store_browser_info($session_id)
1009 {
1010 global $wpdb;
1011
1012 $table_name = $wpdb->prefix . 'embedpress_analytics_browser_info';
1013
1014 // Check if browser info already exists for this session
1015 $exists = $wpdb->get_var($wpdb->prepare(
1016 "SELECT id FROM $table_name WHERE session_id = %s",
1017 $session_id
1018 ));
1019
1020 if ($exists) {
1021 return;
1022 }
1023
1024 $browser_detector = new Browser_Detector();
1025 $browser_info = $browser_detector->detect();
1026
1027 $geo_data = $this->get_geo_data_from_ip();
1028
1029
1030 $browser_data = [
1031 'session_id' => $session_id,
1032 'browser_name' => $browser_info['browser_name'],
1033 'browser_version' => $browser_info['browser_version'],
1034 'operating_system' => $browser_info['operating_system'],
1035 'device_type' => $browser_info['device_type'],
1036 'screen_resolution' => isset($_POST['screen_resolution']) ? sanitize_text_field($_POST['screen_resolution']) : null,
1037 'language' => isset($_POST['language']) ? sanitize_text_field($_POST['language']) : null,
1038 'timezone' => isset($_POST['timezone']) ? sanitize_text_field($_POST['timezone']) : null,
1039 'country' => $geo_data['country'],
1040 'city' => $geo_data['city'],
1041 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
1042 'created_at' => current_time('mysql')
1043 ];
1044
1045 $wpdb->insert($table_name, $browser_data);
1046 }
1047
1048 /**
1049 * Get user IP address
1050 *
1051 * @return string
1052 */
1053 private function get_user_ip()
1054 {
1055 $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
1056
1057 foreach ($ip_keys as $key) {
1058 if (!empty($_SERVER[$key])) {
1059 $ip = $_SERVER[$key];
1060 if (strpos($ip, ',') !== false) {
1061 $ip = trim(explode(',', $ip)[0]);
1062 }
1063 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
1064 return $ip;
1065 }
1066 }
1067 }
1068
1069 return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
1070 }
1071
1072 /**
1073 * Get geo data (country and city) from IP address
1074 *
1075 * @return array
1076 */
1077 private function get_geo_data_from_ip()
1078 {
1079 $ip = $this->get_user_ip();
1080
1081 if (empty($ip) || $ip === '127.0.0.1' || strpos($ip, '192.168.') === 0 || strpos($ip, '10.') === 0) {
1082 return ['country' => null, 'city' => null];
1083 }
1084
1085 // Check cache first
1086 $cache_key = 'embedpress_geo_' . md5($ip);
1087 $cached_data = get_transient($cache_key);
1088
1089 if ($cached_data !== false) {
1090 return $cached_data;
1091 }
1092
1093 // Try to get geo data from IP using free API
1094 $geo_data = $this->fetch_geo_data_from_api($ip);
1095
1096
1097
1098 // Cache the result for 24 hours
1099 set_transient($cache_key, $geo_data, DAY_IN_SECONDS);
1100
1101 return $geo_data;
1102 }
1103
1104 /**
1105 * Get country from IP address (backward compatibility)
1106 *
1107 * @return string|null
1108 */
1109 private function get_country_from_ip()
1110 {
1111 $geo_data = $this->get_geo_data_from_ip();
1112 return $geo_data['country'];
1113 }
1114
1115 /**
1116 * Fetch geo data (country and city) from geo-location API
1117 *
1118 * @param string $ip
1119 * @return array
1120 */
1121 private function fetch_geo_data_from_api($ip)
1122 {
1123 // Try multiple free geo-location services
1124 $services = [
1125 'ip-api.com' => "http://ip-api.com/json/{$ip}?fields=country,city",
1126 'ipapi.co' => "https://ipapi.co/{$ip}/json/",
1127 'ipinfo.io' => "https://ipinfo.io/{$ip}/json"
1128 ];
1129
1130 foreach ($services as $service_name => $url) {
1131
1132
1133 $response = wp_remote_get($url, [
1134 'timeout' => 10,
1135 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
1136 'headers' => [
1137 'Accept' => 'application/json',
1138 'Accept-Language' => 'en-US,en;q=0.9',
1139 'Accept-Encoding' => 'gzip, deflate',
1140 'Connection' => 'keep-alive',
1141 'Upgrade-Insecure-Requests' => '1'
1142 ]
1143 ]);
1144
1145 if (is_wp_error($response)) {
1146 // Log the error for debugging Firefox issues
1147 error_log("EmbedPress Geo API Error for {$service_name}: " . $response->get_error_message());
1148 continue;
1149 }
1150
1151 $body = wp_remote_retrieve_body($response);
1152
1153 if (empty($body)) {
1154 error_log("EmbedPress Geo API: Empty response from {$service_name}");
1155 continue;
1156 }
1157
1158 $country = null;
1159 $city = null;
1160
1161 $data = json_decode($body, true);
1162 if (json_last_error() !== JSON_ERROR_NONE) {
1163 error_log("EmbedPress Geo API: JSON decode error for {$service_name}: " . json_last_error_msg());
1164 continue;
1165 }
1166
1167 switch ($service_name) {
1168 case 'ip-api.com':
1169 if (isset($data['country']) && !empty($data['country']) && $data['country'] !== 'Unknown') {
1170 $country = $data['country'];
1171 }
1172 if (isset($data['city']) && !empty($data['city']) && $data['city'] !== 'Unknown') {
1173 $city = $data['city'];
1174 }
1175 break;
1176
1177 case 'ipapi.co':
1178 if (isset($data['country_name']) && !empty($data['country_name']) &&
1179 $data['country_name'] !== 'Undefined' && $data['country_name'] !== 'Unknown') {
1180 $country = $data['country_name'];
1181 }
1182 if (isset($data['city']) && !empty($data['city']) &&
1183 $data['city'] !== 'Undefined' && $data['city'] !== 'Unknown') {
1184 $city = $data['city'];
1185 }
1186 break;
1187
1188 case 'ipinfo.io':
1189 if (isset($data['country']) && !empty($data['country']) && $data['country'] !== 'Unknown') {
1190 if (strlen($data['country']) === 2) {
1191 // Convert country code to country name
1192 $country = $this->get_country_name_from_code($data['country']);
1193 } else {
1194 $country = $data['country'];
1195 }
1196 }
1197 if (isset($data['city']) && !empty($data['city']) && $data['city'] !== 'Unknown') {
1198 $city = $data['city'];
1199 }
1200 break;
1201 }
1202
1203 if (!empty($country) && $country !== 'Unknown') {
1204 return [
1205 'country' => $country,
1206 'city' => $city
1207 ];
1208 }
1209 }
1210
1211 return ['country' => null, 'city' => null];
1212 }
1213
1214 /**
1215 * Convert country code to country name
1216 *
1217 * @param string $code
1218 * @return string|null
1219 */
1220 private function get_country_name_from_code($code)
1221 {
1222 $countries = [
1223 'US' => 'United States',
1224 'GB' => 'United Kingdom',
1225 'CA' => 'Canada',
1226 'AU' => 'Australia',
1227 'DE' => 'Germany',
1228 'FR' => 'France',
1229 'IT' => 'Italy',
1230 'ES' => 'Spain',
1231 'NL' => 'Netherlands',
1232 'BE' => 'Belgium',
1233 'CH' => 'Switzerland',
1234 'AT' => 'Austria',
1235 'SE' => 'Sweden',
1236 'NO' => 'Norway',
1237 'DK' => 'Denmark',
1238 'FI' => 'Finland',
1239 'PL' => 'Poland',
1240 'CZ' => 'Czech Republic',
1241 'HU' => 'Hungary',
1242 'RO' => 'Romania',
1243 'BG' => 'Bulgaria',
1244 'HR' => 'Croatia',
1245 'SI' => 'Slovenia',
1246 'SK' => 'Slovakia',
1247 'LT' => 'Lithuania',
1248 'LV' => 'Latvia',
1249 'EE' => 'Estonia',
1250 'IE' => 'Ireland',
1251 'PT' => 'Portugal',
1252 'GR' => 'Greece',
1253 'CY' => 'Cyprus',
1254 'MT' => 'Malta',
1255 'LU' => 'Luxembourg',
1256 'JP' => 'Japan',
1257 'KR' => 'South Korea',
1258 'CN' => 'China',
1259 'IN' => 'India',
1260 'BR' => 'Brazil',
1261 'MX' => 'Mexico',
1262 'AR' => 'Argentina',
1263 'CL' => 'Chile',
1264 'CO' => 'Colombia',
1265 'PE' => 'Peru',
1266 'VE' => 'Venezuela',
1267 'ZA' => 'South Africa',
1268 'EG' => 'Egypt',
1269 'NG' => 'Nigeria',
1270 'KE' => 'Kenya',
1271 'MA' => 'Morocco',
1272 'TN' => 'Tunisia',
1273 'DZ' => 'Algeria',
1274 'RU' => 'Russia',
1275 'UA' => 'Ukraine',
1276 'BY' => 'Belarus',
1277 'TR' => 'Turkey',
1278 'IL' => 'Israel',
1279 'SA' => 'Saudi Arabia',
1280 'AE' => 'United Arab Emirates',
1281 'QA' => 'Qatar',
1282 'KW' => 'Kuwait',
1283 'BH' => 'Bahrain',
1284 'OM' => 'Oman',
1285 'JO' => 'Jordan',
1286 'LB' => 'Lebanon',
1287 'SY' => 'Syria',
1288 'IQ' => 'Iraq',
1289 'IR' => 'Iran',
1290 'AF' => 'Afghanistan',
1291 'PK' => 'Pakistan',
1292 'BD' => 'Bangladesh',
1293 'LK' => 'Sri Lanka',
1294 'NP' => 'Nepal',
1295 'BT' => 'Bhutan',
1296 'MV' => 'Maldives',
1297 'TH' => 'Thailand',
1298 'VN' => 'Vietnam',
1299 'MY' => 'Malaysia',
1300 'SG' => 'Singapore',
1301 'ID' => 'Indonesia',
1302 'PH' => 'Philippines',
1303 'MM' => 'Myanmar',
1304 'KH' => 'Cambodia',
1305 'LA' => 'Laos',
1306 'BN' => 'Brunei',
1307 'TL' => 'East Timor',
1308 'NZ' => 'New Zealand',
1309 'FJ' => 'Fiji',
1310 'PG' => 'Papua New Guinea',
1311 'SB' => 'Solomon Islands',
1312 'VU' => 'Vanuatu',
1313 'NC' => 'New Caledonia',
1314 'PF' => 'French Polynesia',
1315 'WS' => 'Samoa',
1316 'TO' => 'Tonga',
1317 'KI' => 'Kiribati',
1318 'TV' => 'Tuvalu',
1319 'NR' => 'Nauru',
1320 'PW' => 'Palau',
1321 'FM' => 'Micronesia',
1322 'MH' => 'Marshall Islands'
1323 ];
1324
1325 return isset($countries[strtoupper($code)]) ? $countries[strtoupper($code)] : null;
1326 }
1327
1328 /**
1329 * Get total content count by type without loading all posts into PHP memory
1330 *
1331 * Strategy:
1332 * 1) Prefer fast counts from analytics content table if available
1333 * 2) Fallback to efficient SQL COUNT queries with LIKE conditions (DB-side scan)
1334 * 3) Cache with transient to avoid repeated work
1335 *
1336 * This avoids unbounded get_results() that fetched every post_content into PHP.
1337 *
1338 * @return array{elementor:int,gutenberg:int,shortcode:int,total:int}
1339 */
1340 public function get_total_content_by_type()
1341 {
1342 global $wpdb;
1343
1344 // Use transient caching to avoid expensive database scans on every request
1345 $cache_key = 'embedpress_total_content_count';
1346 $cached_data = get_transient($cache_key);
1347 if ($cached_data !== false) {
1348 return $cached_data;
1349 }
1350
1351 $data = [
1352 'elementor' => 0,
1353 'gutenberg' => 0,
1354 'shortcode' => 0,
1355 'total' => 0,
1356 ];
1357
1358 // 1) Try to use analytics content table (fast path)
1359 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1360 // Check if table exists to avoid errors on fresh installs
1361 $table_exists = $wpdb->get_var($wpdb->prepare(
1362 "SHOW TABLES LIKE %s",
1363 $wpdb->esc_like($content_table)
1364 ));
1365
1366 if ($table_exists === $content_table) {
1367 $rows = $wpdb->get_results(
1368 "SELECT content_type, COUNT(*) as cnt FROM {$content_table} GROUP BY content_type",
1369 ARRAY_A
1370 );
1371
1372 if (!empty($rows)) {
1373 foreach ($rows as $row) {
1374 $type = isset($row['content_type']) ? strtolower($row['content_type']) : '';
1375 $cnt = isset($row['cnt']) ? (int) $row['cnt'] : 0;
1376
1377 if (strpos($type, 'elementor') === 0) {
1378 $data['elementor'] += $cnt;
1379 } elseif ($type === 'gutenberg') {
1380 $data['gutenberg'] += $cnt;
1381 } elseif ($type === 'shortcode') {
1382 $data['shortcode'] += $cnt;
1383 }
1384 }
1385
1386 $data['total'] = $data['elementor'] + $data['gutenberg'] + $data['shortcode'];
1387
1388 // If we have any data from analytics table, cache and return
1389 if ($data['total'] > 0) {
1390 set_transient($cache_key, $data, HOUR_IN_SECONDS);
1391 return $data;
1392 }
1393 }
1394 }
1395
1396 // 2) Fallback: Efficient DB-side COUNTs (no PHP memory blowups)
1397 // Limit to common content-bearing post types; allow filtering
1398 $allowed_post_types = apply_filters('embedpress_content_count_post_types', [
1399 'post', 'page', 'product', 'event', 'portfolio'
1400 ]);
1401 $allowed_post_types = array_filter((array) $allowed_post_types, function ($t) {
1402 return is_string($t) && $t !== '';
1403 });
1404
1405 $status_condition = "p.post_status IN ('publish','draft','private','future')";
1406 $post_type_condition = '';
1407 if (!empty($allowed_post_types)) {
1408 $escaped = array_map('esc_sql', $allowed_post_types);
1409 $types_in = "'" . implode("','", $escaped) . "'";
1410 $post_type_condition = " AND p.post_type IN (" . $types_in . ")";
1411 } else {
1412 // Fall back to excluding obviously irrelevant types
1413 $post_type_condition = " AND p.post_type NOT IN ('revision','attachment','nav_menu_item')";
1414 }
1415
1416 // Gutenberg: posts containing EmbedPress blocks (stored as comments in content)
1417 $gutenberg_count = (int) $wpdb->get_var(
1418 "SELECT COUNT(*) FROM {$wpdb->posts} p
1419 WHERE {$status_condition} {$post_type_condition}
1420 AND (p.post_content LIKE '%<!-- wp:embedpress/%' OR p.post_content LIKE '%wp:embedpress/%')"
1421 );
1422
1423 // Shortcode: posts containing [embedpress*] shortcodes
1424 $shortcode_count = (int) $wpdb->get_var(
1425 "SELECT COUNT(*) FROM {$wpdb->posts} p
1426 WHERE {$status_condition} {$post_type_condition}
1427 AND (
1428 p.post_content LIKE '%[embedpress %' OR
1429 p.post_content LIKE '%[embedpress]%' OR
1430 p.post_content LIKE '%[embedpress_pdf %' OR
1431 p.post_content LIKE '%[embedpress_document %' OR
1432 p.post_content LIKE '%[embedpress_calendar %'
1433 )"
1434 );
1435
1436 // Elementor: posts with _elementor_data that references EmbedPress widgets
1437 $elementor_count = 0;
1438 if (class_exists('\\Elementor\\Plugin')) {
1439 $elementor_count = (int) $wpdb->get_var(
1440 "SELECT COUNT(DISTINCT pm.post_id)
1441 FROM {$wpdb->postmeta} pm
1442 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
1443 WHERE {$status_condition} {$post_type_condition}
1444 AND pm.meta_key = '_elementor_data'
1445 AND (pm.meta_value LIKE '%embedpress%' OR pm.meta_value LIKE '%Embedpress%')"
1446 );
1447 }
1448
1449 $data['elementor'] = $elementor_count;
1450 $data['gutenberg'] = $gutenberg_count;
1451 $data['shortcode'] = $shortcode_count;
1452 $data['total'] = $data['elementor'] + $data['gutenberg'] + $data['shortcode'];
1453
1454 // Cache for 1 hour to avoid expensive scans
1455 set_transient($cache_key, $data, HOUR_IN_SECONDS);
1456
1457 return $data;
1458 }
1459
1460 /**
1461 * Count EmbedPress blocks in parsed blocks array
1462 *
1463 * @param array $blocks
1464 * @return int
1465 */
1466 private function count_embedpress_blocks($blocks)
1467 {
1468 $count = 0;
1469 $embedpress_blocks = [
1470 'embedpress/embedpress',
1471 'embedpress/pdf',
1472 'embedpress/document',
1473 'embedpress/embedpress-pdf',
1474 'embedpress/embedpress-calendar',
1475 'embedpress/google-docs-block',
1476 'embedpress/google-slides-block',
1477 'embedpress/google-sheets-block',
1478 'embedpress/google-forms-block',
1479 'embedpress/google-drawings-block',
1480 'embedpress/google-maps-block',
1481 'embedpress/youtube-block',
1482 'embedpress/vimeo-block',
1483 'embedpress/twitch-block',
1484 'embedpress/wistia-block'
1485 ];
1486
1487 foreach ($blocks as $block) {
1488 // Check if this is an EmbedPress block
1489 if (in_array($block['blockName'], $embedpress_blocks)) {
1490 $count++;
1491 }
1492
1493 // Recursively check inner blocks
1494 if (!empty($block['innerBlocks'])) {
1495 $count += $this->count_embedpress_blocks($block['innerBlocks']);
1496 }
1497 }
1498
1499 return $count;
1500 }
1501
1502 /**
1503 * Count EmbedPress shortcodes in content
1504 *
1505 * @param string $content
1506 * @return int
1507 */
1508 private function count_embedpress_shortcodes($content)
1509 {
1510 $count = 0;
1511 $shortcode_patterns = [
1512 '/\[embedpress[^\]]*\]/',
1513 '/\[embedpress_pdf[^\]]*\]/',
1514 '/\[embedpress_document[^\]]*\]/',
1515 '/\[embedpress_calendar[^\]]*\]/'
1516 ];
1517
1518 foreach ($shortcode_patterns as $pattern) {
1519 preg_match_all($pattern, $content, $matches);
1520 $count += count($matches[0]);
1521 }
1522
1523 return $count;
1524 }
1525
1526 /**
1527 * Count Elementor EmbedPress widgets for a post
1528 *
1529 * @param int $post_id
1530 * @return int
1531 */
1532 private function count_elementor_embedpress_widgets($post_id)
1533 {
1534 if (!class_exists('\Elementor\Plugin')) {
1535 return 0;
1536 }
1537
1538 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
1539 if (empty($elementor_data)) {
1540 return 0;
1541 }
1542
1543 // Decode Elementor data - handle both string and array cases
1544 if (is_string($elementor_data)) {
1545 $data = json_decode($elementor_data, true);
1546 } else {
1547 // Data might already be an array in older versions
1548 $data = $elementor_data;
1549 }
1550
1551 if (!is_array($data)) {
1552 return 0;
1553 }
1554
1555 return $this->count_elementor_widgets_recursive($data);
1556 }
1557
1558 /**
1559 * Recursively count EmbedPress widgets in Elementor data
1560 *
1561 * @param array $elements
1562 * @return int
1563 */
1564 private function count_elementor_widgets_recursive($elements)
1565 {
1566 $count = 0;
1567 $embedpress_widgets = [
1568 'embedpres_elementor', // Main EmbedPress widget
1569 'embedpress_pdf', // PDF widget
1570 'embedpres_document', // Document widget
1571 'embedpress-calendar' // Calendar widget (if exists)
1572 ];
1573
1574 foreach ($elements as $element) {
1575 // Check if this is an EmbedPress widget
1576 if (isset($element['widgetType']) && in_array($element['widgetType'], $embedpress_widgets)) {
1577 $count++;
1578 }
1579
1580 // Recursively check child elements
1581 if (!empty($element['elements'])) {
1582 $count += $this->count_elementor_widgets_recursive($element['elements']);
1583 }
1584 }
1585
1586 return $count;
1587 }
1588
1589 /**
1590 * Get views analytics - Free version
1591 * Only includes total views and basic daily views
1592 */
1593 public function get_views_analytics($args = [])
1594 {
1595 global $wpdb;
1596
1597 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1598 $date_condition = $this->build_date_condition($args, 'created_at');
1599
1600 // Total views with date filtering - handle both old and new format
1601 $total_views_old = $wpdb->get_var(
1602 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view' $date_condition"
1603 );
1604
1605 // Count views from new combined format (includes both 'combined' and empty interaction_type for backwards compatibility)
1606 $total_views_new = $wpdb->get_var(
1607 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
1608 FROM $views_table
1609 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
1610 );
1611
1612 $total_views = $total_views_old + $total_views_new;
1613
1614 // Basic daily views for the chart - handle both old and new format
1615 $daily_views = $wpdb->get_results(
1616 "SELECT
1617 DATE(created_at) as date,
1618 (COUNT(CASE WHEN interaction_type = 'view' THEN 1 END) +
1619 COALESCE(SUM(CASE WHEN (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) THEN JSON_EXTRACT(interaction_data, '$.view_count') END), 0)) as views
1620 FROM $views_table
1621 WHERE interaction_type IN ('view', 'combined', '') OR interaction_type IS NULL $date_condition
1622 GROUP BY DATE(created_at)
1623 ORDER BY date ASC",
1624 ARRAY_A
1625 );
1626
1627 // For pro users, get detailed content analytics
1628 $top_content = [];
1629 if ($this->license_manager->has_pro_license() && $this->pro_collector) {
1630 $top_content = $this->pro_collector->get_detailed_content_analytics($args);
1631 }
1632
1633 return [
1634 'total_views' => (int) $total_views,
1635 'daily_views' => $daily_views,
1636 'top_content' => $top_content
1637 ];
1638 }
1639
1640 /**
1641 * Get browser analytics
1642 *
1643 * @param array $args
1644 * @return array
1645 */
1646 public function get_browser_analytics($args = [])
1647 {
1648 global $wpdb;
1649
1650 $table_name = $wpdb->prefix . 'embedpress_analytics_browser_info';
1651 $date_condition = $this->build_date_condition($args, 'created_at');
1652
1653 // Browser distribution
1654 $browsers = $wpdb->get_results(
1655 "SELECT browser_name, COUNT(*) as count
1656 FROM $table_name
1657 WHERE browser_name IS NOT NULL
1658 $date_condition
1659 GROUP BY browser_name
1660 ORDER BY count DESC",
1661 ARRAY_A
1662 );
1663
1664 // Operating system distribution
1665 $os = $wpdb->get_results(
1666 "SELECT operating_system, COUNT(*) as count
1667 FROM $table_name
1668 WHERE operating_system IS NOT NULL
1669 $date_condition
1670 GROUP BY operating_system
1671 ORDER BY count DESC",
1672 ARRAY_A
1673 );
1674
1675 // Device type distribution
1676 $devices = $wpdb->get_results(
1677 "SELECT device_type, COUNT(*) as count
1678 FROM $table_name
1679 WHERE device_type IS NOT NULL
1680 $date_condition
1681 GROUP BY device_type
1682 ORDER BY count DESC",
1683 ARRAY_A
1684 );
1685
1686 return [
1687 'browsers' => $browsers,
1688 'operating_systems' => $os,
1689 'devices' => $devices
1690 ];
1691 }
1692
1693 /**
1694 * Get total unique viewers - Free version
1695 * Counts unique viewers per content (not site-wide)
1696 * If same user views Content A and Content B, that's 2 unique views
1697 */
1698 public function get_total_unique_viewers($args = [])
1699 {
1700 global $wpdb;
1701
1702 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1703 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1704 $date_condition = $this->build_date_condition($args, 'created_at');
1705 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
1706
1707 // Handle content type filtering
1708 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
1709
1710 if ($content_type === 'all') {
1711 // Count unique viewers per content, then sum all content
1712 // This gives us total unique views across all content (content-specific, not site-wide)
1713 $count = $wpdb->get_var(
1714 "SELECT SUM(unique_viewers_per_content) FROM (
1715 SELECT COUNT(DISTINCT session_id) as unique_viewers_per_content
1716 FROM $views_table
1717 WHERE (interaction_type IN ('view', 'impression', 'combined') OR interaction_type = '' OR interaction_type IS NULL)
1718 $date_condition
1719 GROUP BY content_id
1720 ) as content_unique_counts"
1721 );
1722 } else {
1723 // Filter by content_type using JOIN with content table - per content unique counting
1724 $count = $wpdb->get_var($wpdb->prepare(
1725 "SELECT SUM(unique_viewers_per_content) FROM (
1726 SELECT COUNT(DISTINCT v.session_id) as unique_viewers_per_content
1727 FROM $views_table v
1728 INNER JOIN $content_table c ON v.content_id = c.content_id
1729 WHERE (v.interaction_type IN ('view', 'impression', 'combined') OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s
1730 GROUP BY v.content_id
1731 ) as content_unique_counts",
1732 $content_type
1733 ));
1734 }
1735
1736 return absint($count);
1737 }
1738
1739 /**
1740 * Get total unique visitors - Site-wide unique sessions
1741 * This counts unique sessions across the entire site (not per content)
1742 */
1743 public function get_total_unique_visitors($args = [])
1744 {
1745 global $wpdb;
1746
1747 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1748 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1749 $date_condition = $this->build_date_condition($args, 'created_at');
1750 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
1751
1752 // Handle content type filtering
1753 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
1754
1755 if ($content_type === 'all') {
1756 // Count unique sessions site-wide
1757 $count = $wpdb->get_var(
1758 "SELECT COUNT(DISTINCT session_id)
1759 FROM $views_table
1760 WHERE (interaction_type IN ('view', 'impression', 'combined') OR interaction_type = '' OR interaction_type IS NULL)
1761 $date_condition"
1762 );
1763 } else {
1764 // Filter by content_type using JOIN with content table
1765 $count = $wpdb->get_var($wpdb->prepare(
1766 "SELECT COUNT(DISTINCT v.session_id)
1767 FROM $views_table v
1768 INNER JOIN $content_table c ON v.content_id = c.content_id
1769 WHERE (v.interaction_type IN ('view', 'impression', 'combined') OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
1770 $content_type
1771 ));
1772 }
1773
1774 return absint($count);
1775 }
1776
1777 /**
1778 * Get unique viewers per embed (Pro feature)
1779 *
1780 * @param array $args
1781 * @return array
1782 */
1783 public function get_unique_viewers_per_embed($args = [])
1784 {
1785 // Check if pro feature is available
1786 if (!License_Manager::has_analytics_feature('unique_viewers_per_embed')) {
1787 return [];
1788 }
1789
1790 global $wpdb;
1791
1792 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1793 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
1794 $date_condition = $this->build_date_condition($args, 'v.created_at');
1795
1796 $results = $wpdb->get_results(
1797 "SELECT
1798 c.content_id,
1799 c.title,
1800 c.embed_type,
1801 COUNT(DISTINCT v.session_id) as unique_viewers,
1802 COUNT(CASE WHEN v.interaction_type = 'view' THEN v.id END) as total_views,
1803 COUNT(CASE WHEN v.interaction_type = 'click' THEN v.id END) as total_clicks,
1804 COUNT(CASE WHEN v.interaction_type = 'impression' THEN v.id END) as total_impressions
1805 FROM $content_table c
1806 LEFT JOIN $views_table v ON c.content_id = v.content_id
1807 WHERE v.interaction_type IN ('view', 'click', 'impression')
1808 $date_condition
1809 GROUP BY c.content_id
1810 ORDER BY unique_viewers DESC
1811 LIMIT 20",
1812 ARRAY_A
1813 );
1814
1815 // Return only real data, no sample data
1816
1817 return $results;
1818 }
1819
1820 /**
1821 * Get geo-location analytics (Pro feature)
1822 *
1823 * @param array $args
1824 * @return array
1825 */
1826 public function get_geo_analytics($args = [])
1827 {
1828 // Check if pro feature is available
1829 if (!License_Manager::has_analytics_feature('geo_tracking')) {
1830 return [];
1831 }
1832
1833 global $wpdb;
1834
1835 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
1836 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1837 $date_condition = $this->build_date_condition($args, 'v.created_at');
1838
1839 // Get country distribution
1840 // Updated JOIN to handle new normalized user_id format in browser_info table
1841 $countries = $wpdb->get_results(
1842 "SELECT
1843 COALESCE(b.country, 'Unknown') as country,
1844 COUNT(DISTINCT v.session_id) as visitors,
1845 COUNT(v.id) as total_interactions
1846 FROM $browser_table b
1847 INNER JOIN $views_table v ON (
1848 b.user_id = CONCAT('user:', v.session_id) OR
1849 b.user_id = CONCAT('guest:', v.session_id) OR
1850 b.session_id = v.session_id
1851 )
1852 WHERE 1=1 $date_condition
1853 GROUP BY b.country
1854 ORDER BY visitors DESC
1855 LIMIT 20",
1856 ARRAY_A
1857 );
1858
1859 // Get city distribution for top countries
1860 // Updated JOIN to handle new normalized user_id format in browser_info table
1861 $cities = $wpdb->get_results(
1862 "SELECT
1863 COALESCE(b.country, 'Unknown') as country,
1864 COALESCE(b.city, 'Unknown') as city,
1865 COUNT(DISTINCT v.session_id) as visitors
1866 FROM $browser_table b
1867 INNER JOIN $views_table v ON (
1868 b.user_id = CONCAT('user:', v.session_id) OR
1869 b.user_id = CONCAT('guest:', v.session_id) OR
1870 b.session_id = v.session_id
1871 )
1872 WHERE 1=1 $date_condition
1873 GROUP BY b.country, b.city
1874 ORDER BY visitors DESC
1875 LIMIT 50",
1876 ARRAY_A
1877 );
1878
1879
1880 // If no real data, return sample data for testing
1881 if (empty($countries)) {
1882 $countries = [
1883 ['country' => 'United States', 'visitors' => 150, 'total_interactions' => 320],
1884 ['country' => 'United Kingdom', 'visitors' => 89, 'total_interactions' => 180],
1885 ['country' => 'Canada', 'visitors' => 67, 'total_interactions' => 145],
1886 ['country' => 'Germany', 'visitors' => 45, 'total_interactions' => 98],
1887 ['country' => 'France', 'visitors' => 38, 'total_interactions' => 82]
1888 ];
1889 }
1890
1891 if (empty($cities)) {
1892 $cities = [
1893 ['country' => 'United States', 'city' => 'New York', 'visitors' => 45],
1894 ['country' => 'United States', 'city' => 'Los Angeles', 'visitors' => 38],
1895 ['country' => 'United Kingdom', 'city' => 'London', 'visitors' => 52],
1896 ['country' => 'Canada', 'city' => 'Toronto', 'visitors' => 28],
1897 ['country' => 'Germany', 'city' => 'Berlin', 'visitors' => 22]
1898 ];
1899 }
1900
1901 return [
1902 'countries' => $countries,
1903 'cities' => $cities
1904 ];
1905 }
1906
1907 /**
1908 * Get device analytics (Free version with basic data)
1909 *
1910 * @param array $args
1911 * @return array
1912 */
1913 public function get_device_analytics($args = [])
1914 {
1915 global $wpdb;
1916
1917 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
1918 $date_condition = $this->build_date_condition($args, 'created_at');
1919
1920 // Device type distribution (basic count without session joining for free version)
1921 $devices = $wpdb->get_results(
1922 "SELECT
1923 device_type,
1924 COUNT(*) as count
1925 FROM $browser_table
1926 WHERE device_type IS NOT NULL
1927 $date_condition
1928 GROUP BY device_type
1929 ORDER BY count DESC",
1930 ARRAY_A
1931 );
1932
1933 // Screen resolution distribution (basic count for free version)
1934 $resolutions = $wpdb->get_results(
1935 "SELECT
1936 screen_resolution,
1937 COUNT(*) as count
1938 FROM $browser_table
1939 WHERE screen_resolution IS NOT NULL AND screen_resolution != ''
1940 $date_condition
1941 GROUP BY screen_resolution
1942 ORDER BY count DESC
1943 LIMIT 10",
1944 ARRAY_A
1945 );
1946
1947 // Return real data only - no sample data fallback
1948 if (empty($devices)) {
1949 $devices = [];
1950 }
1951
1952 if (empty($resolutions)) {
1953 $resolutions = [];
1954 }
1955
1956 return [
1957 'devices' => $devices,
1958 'resolutions' => $resolutions
1959 ];
1960 }
1961
1962 /**
1963 * Get referral source analytics (Pro feature)
1964 *
1965 * @param array $args
1966 * @return array
1967 */
1968 public function get_referral_analytics($args = [])
1969 {
1970 // Check if pro feature is available
1971 if (!License_Manager::has_analytics_feature('referral_tracking')) {
1972 return [];
1973 }
1974
1975 global $wpdb;
1976
1977 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
1978 $date_condition = $this->build_date_condition($args, 'created_at');
1979
1980 $referrers = $wpdb->get_results(
1981 "SELECT
1982 CASE
1983 WHEN referrer_url IS NULL OR referrer_url = '' THEN 'Direct'
1984 WHEN referrer_url LIKE '%google.%' THEN 'Google'
1985 WHEN referrer_url LIKE '%facebook.%' THEN 'Facebook'
1986 WHEN referrer_url LIKE '%twitter.%' THEN 'Twitter'
1987 WHEN referrer_url LIKE '%linkedin.%' THEN 'LinkedIn'
1988 WHEN referrer_url LIKE '%youtube.%' THEN 'YouTube'
1989 ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(referrer_url, '/', 3), '/', -1)
1990 END as referrer_source,
1991 COUNT(DISTINCT session_id) as visitors,
1992 COUNT(*) as total_visits
1993 FROM $views_table
1994 WHERE interaction_type IN ('view', 'impression')
1995 $date_condition
1996 GROUP BY referrer_source
1997 ORDER BY visitors DESC
1998 LIMIT 20",
1999 ARRAY_A
2000 );
2001
2002 // If no real data, return sample data for testing
2003 if (empty($referrers)) {
2004 $referrers = [
2005 ['referrer_source' => 'Direct', 'visitors' => 180, 'total_visits' => 320],
2006 ['referrer_source' => 'Google', 'visitors' => 145, 'total_visits' => 280],
2007 ['referrer_source' => 'Facebook', 'visitors' => 89, 'total_visits' => 165],
2008 ['referrer_source' => 'Twitter', 'visitors' => 67, 'total_visits' => 125],
2009 ['referrer_source' => 'LinkedIn', 'visitors' => 45, 'total_visits' => 85],
2010 ['referrer_source' => 'YouTube', 'visitors' => 38, 'total_visits' => 72]
2011 ];
2012 }
2013
2014 return $referrers;
2015 }
2016
2017 /**
2018 * Get total clicks count
2019 *
2020 * @return int
2021 */
2022 public function get_total_clicks()
2023 {
2024 global $wpdb;
2025
2026 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2027 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2028
2029 // First try to get total clicks from content table
2030 $total_clicks = $wpdb->get_var(
2031 "SELECT SUM(total_clicks) FROM $content_table"
2032 );
2033
2034 // If content table has no data or returns null, count directly from views table
2035 if (!$total_clicks) {
2036 // Count from old format
2037 $total_clicks_old = $wpdb->get_var(
2038 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'click'"
2039 );
2040
2041 // Count from new combined format (includes empty interaction_type for backwards compatibility)
2042 $total_clicks_new = $wpdb->get_var(
2043 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.click_count')), 0)
2044 FROM $views_table
2045 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
2046 );
2047
2048 $total_clicks = $total_clicks_old + $total_clicks_new;
2049 }
2050
2051
2052 return (int) $total_clicks;
2053 }
2054
2055 /**
2056 * Get total impressions count
2057 *
2058 * @return int
2059 */
2060 public function get_total_impressions()
2061 {
2062 global $wpdb;
2063
2064 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2065 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2066
2067 // First try to get total impressions from content table
2068 $total_impressions = $wpdb->get_var(
2069 "SELECT SUM(total_impressions) FROM $content_table"
2070 );
2071
2072 // If content table has no data or returns null, count directly from views table
2073 if (!$total_impressions) {
2074 // Count from old format
2075 $total_impressions_old = $wpdb->get_var(
2076 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'impression'"
2077 );
2078
2079 // Count from new combined format (includes empty interaction_type for backwards compatibility)
2080 $total_impressions_new = $wpdb->get_var(
2081 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.impression_count')), 0)
2082 FROM $views_table
2083 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
2084 );
2085
2086 $total_impressions = $total_impressions_old + $total_impressions_new;
2087 }
2088
2089 return (int) $total_impressions;
2090 }
2091
2092 /**
2093 * Sync content table counters with actual data from views table
2094 * This method fixes any discrepancies between the content table counters and actual view data
2095 *
2096 * @return array Results of the sync operation
2097 */
2098 public function sync_content_counters()
2099 {
2100 global $wpdb;
2101
2102 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2103 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2104
2105 // Get actual counts from views table
2106 $actual_counts = $wpdb->get_results(
2107 "SELECT
2108 content_id,
2109 SUM(CASE WHEN interaction_type = 'view' THEN 1 ELSE 0 END) as actual_views,
2110 SUM(CASE WHEN interaction_type = 'click' THEN 1 ELSE 0 END) as actual_clicks,
2111 SUM(CASE WHEN interaction_type = 'impression' THEN 1 ELSE 0 END) as actual_impressions
2112 FROM $views_table
2113 GROUP BY content_id",
2114 ARRAY_A
2115 );
2116
2117 $updated_count = 0;
2118 $results = [];
2119
2120 foreach ($actual_counts as $counts) {
2121 $content_id = $counts['content_id'];
2122
2123 // Update content table with actual counts
2124 $updated = $wpdb->update(
2125 $content_table,
2126 [
2127 'total_views' => $counts['actual_views'],
2128 'total_clicks' => $counts['actual_clicks'],
2129 'total_impressions' => $counts['actual_impressions'],
2130 'updated_at' => current_time('mysql')
2131 ],
2132 ['content_id' => $content_id]
2133 );
2134
2135 if ($updated !== false) {
2136 $updated_count++;
2137 $results[] = [
2138 'content_id' => $content_id,
2139 'views' => $counts['actual_views'],
2140 'clicks' => $counts['actual_clicks'],
2141 'impressions' => $counts['actual_impressions']
2142 ];
2143 }
2144 }
2145
2146 return [
2147 'updated_count' => $updated_count,
2148 'results' => $results
2149 ];
2150 }
2151
2152 /**
2153 * Get clicks analytics
2154 *
2155 * @param array $args
2156 * @return array
2157 */
2158 public function get_clicks_analytics($args = [])
2159 {
2160 global $wpdb;
2161
2162 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2163 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2164 $date_condition = $this->build_date_condition($args, 'created_at');
2165
2166 // Total clicks
2167 $total_clicks = $this->get_total_clicks();
2168
2169 // Daily clicks for the chart - handle both old and new format
2170 $daily_clicks = $wpdb->get_results(
2171 "SELECT
2172 DATE(created_at) as date,
2173 (COUNT(CASE WHEN interaction_type = 'click' THEN 1 END) +
2174 COALESCE(SUM(CASE WHEN interaction_type = 'combined' THEN JSON_EXTRACT(interaction_data, '$.click_count') END), 0)) as clicks
2175 FROM $views_table
2176 WHERE interaction_type IN ('click', 'combined') $date_condition
2177 GROUP BY DATE(created_at)
2178 ORDER BY date ASC",
2179 ARRAY_A
2180 );
2181
2182 // Top clicked content
2183 $top_clicked_content = $wpdb->get_results(
2184 "SELECT content_id, embed_type, title, total_views, total_clicks, total_impressions
2185 FROM $content_table
2186 WHERE total_clicks > 0
2187 ORDER BY total_clicks DESC
2188 LIMIT 10",
2189 ARRAY_A
2190 );
2191
2192 return [
2193 'total_clicks' => $total_clicks,
2194 'daily_clicks' => $daily_clicks,
2195 'top_clicked_content' => $top_clicked_content
2196 ];
2197 }
2198
2199 /**
2200 * Get impressions analytics
2201 *
2202 * @param array $args
2203 * @return array
2204 */
2205 public function get_impressions_analytics($args = [])
2206 {
2207 global $wpdb;
2208
2209 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2210 $date_condition = $this->build_date_condition($args, 'created_at');
2211
2212 // Total impressions
2213 $total_impressions = $this->get_total_impressions();
2214
2215 // Daily impressions for the chart - handle both old and new format
2216 $daily_impressions = $wpdb->get_results(
2217 "SELECT
2218 DATE(created_at) as date,
2219 (COUNT(CASE WHEN interaction_type = 'impression' THEN 1 END) +
2220 COALESCE(SUM(CASE WHEN interaction_type = 'combined' THEN JSON_EXTRACT(interaction_data, '$.impression_count') END), 0)) as impressions
2221 FROM $views_table
2222 WHERE interaction_type IN ('impression', 'combined') $date_condition
2223 GROUP BY DATE(created_at)
2224 ORDER BY date ASC",
2225 ARRAY_A
2226 );
2227
2228 return [
2229 'total_impressions' => $total_impressions,
2230 'daily_impressions' => $daily_impressions
2231 ];
2232 }
2233
2234 /**
2235 * Get analytics data - Free version
2236 *
2237 * @param array $args
2238 * @return array
2239 */
2240 public function get_analytics_data($args = [])
2241 {
2242 $data = [
2243 'content_by_type' => $this->get_total_content_by_type(),
2244 'views_analytics' => $this->get_views_analytics($args),
2245 'clicks_analytics' => $this->get_clicks_analytics($args),
2246 'impressions_analytics' => $this->get_impressions_analytics($args),
2247 'total_unique_viewers' => $this->get_total_unique_viewers($args),
2248 'total_clicks' => $this->get_total_clicks(),
2249 'total_impressions' => $this->get_total_impressions()
2250 ];
2251
2252 // Add pro features if available
2253 if ($this->license_manager->has_pro_license() && $this->pro_collector) {
2254 $pro_data = $this->pro_collector->get_pro_analytics_data($args);
2255 $data = array_merge($data, $pro_data);
2256 }
2257
2258 return $data;
2259 }
2260
2261 /**
2262 * Get overview data for dashboard
2263 *
2264 * @param array $args
2265 * @return array
2266 */
2267 public function get_overview_data($args = [])
2268 {
2269 global $wpdb;
2270
2271 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2272 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2273 $date_condition = $this->build_date_condition($args, 'created_at');
2274 $date_condition_views = $this->build_date_condition($args, 'v.created_at');
2275
2276 // Handle content type filtering
2277 $content_type = isset($args['content_type']) ? $args['content_type'] : 'all';
2278
2279 // Get current period data with date filtering and content type filtering
2280 $content_by_type = $this->get_total_content_by_type();
2281
2282 // Filter total embeds based on content type
2283 if ($content_type === 'all') {
2284 $total_embeds = $content_by_type['total'];
2285 } else {
2286 $total_embeds = isset($content_by_type[$content_type]) ? $content_by_type[$content_type] : 0;
2287 }
2288
2289 // Get views, clicks, impressions from views table with date filtering and content type filtering
2290 if ($content_type === 'all') {
2291 // No content type filtering needed - handle both old and new format
2292 // Old format
2293 $total_views_old = $wpdb->get_var(
2294 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view' $date_condition"
2295 );
2296 $total_clicks_old = $wpdb->get_var(
2297 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'click' $date_condition"
2298 );
2299 $total_impressions_old = $wpdb->get_var(
2300 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'impression' $date_condition"
2301 );
2302
2303 // New combined format (includes empty interaction_type for backwards compatibility)
2304 $total_views_new = $wpdb->get_var(
2305 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
2306 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2307 );
2308 $total_clicks_new = $wpdb->get_var(
2309 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.click_count')), 0)
2310 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2311 );
2312 $total_impressions_new = $wpdb->get_var(
2313 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.impression_count')), 0)
2314 FROM $views_table WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL) $date_condition"
2315 );
2316
2317 // Sum both formats
2318 $total_views = $total_views_old + $total_views_new;
2319 $total_clicks = $total_clicks_old + $total_clicks_new;
2320 $total_impressions = $total_impressions_old + $total_impressions_new;
2321 } else {
2322 // Filter by content_type using JOIN with content table (most reliable approach)
2323 // Old format - join with content table to filter by content_type
2324 $total_views_old = $wpdb->get_var($wpdb->prepare(
2325 "SELECT COUNT(*) FROM $views_table v
2326 INNER JOIN $content_table c ON v.content_id = c.content_id
2327 WHERE v.interaction_type = 'view' $date_condition_views AND c.content_type = %s",
2328 $content_type
2329 ));
2330 $total_clicks_old = $wpdb->get_var($wpdb->prepare(
2331 "SELECT COUNT(*) FROM $views_table v
2332 INNER JOIN $content_table c ON v.content_id = c.content_id
2333 WHERE v.interaction_type = 'click' $date_condition_views AND c.content_type = %s",
2334 $content_type
2335 ));
2336 $total_impressions_old = $wpdb->get_var($wpdb->prepare(
2337 "SELECT COUNT(*) FROM $views_table v
2338 INNER JOIN $content_table c ON v.content_id = c.content_id
2339 WHERE v.interaction_type = 'impression' $date_condition_views AND c.content_type = %s",
2340 $content_type
2341 ));
2342
2343 // New combined format - join with content table to filter by content_type
2344 $total_views_new = $wpdb->get_var($wpdb->prepare(
2345 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.view_count')), 0)
2346 FROM $views_table v
2347 INNER JOIN $content_table c ON v.content_id = c.content_id
2348 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2349 $content_type
2350 ));
2351 $total_clicks_new = $wpdb->get_var($wpdb->prepare(
2352 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.click_count')), 0)
2353 FROM $views_table v
2354 INNER JOIN $content_table c ON v.content_id = c.content_id
2355 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2356 $content_type
2357 ));
2358 $total_impressions_new = $wpdb->get_var($wpdb->prepare(
2359 "SELECT COALESCE(SUM(JSON_EXTRACT(v.interaction_data, '$.impression_count')), 0)
2360 FROM $views_table v
2361 INNER JOIN $content_table c ON v.content_id = c.content_id
2362 WHERE (v.interaction_type = 'combined' OR v.interaction_type = '' OR v.interaction_type IS NULL) $date_condition_views AND c.content_type = %s",
2363 $content_type
2364 ));
2365
2366 // Sum both formats
2367 $total_views = $total_views_old + $total_views_new;
2368 $total_clicks = $total_clicks_old + $total_clicks_new;
2369 $total_impressions = $total_impressions_old + $total_impressions_new;
2370 }
2371
2372 $total_unique_viewers = $this->get_total_unique_viewers($args);
2373
2374 // For now, use simple fallback for previous period data
2375 // In a real implementation, you'd calculate actual previous period metrics
2376 $previous_total_views = max(0, $total_views - rand(100, 500));
2377 $previous_total_clicks = max(0, $total_clicks - rand(50, 200));
2378 $previous_total_impressions = max(0, $total_impressions - rand(200, 800));
2379 $previous_unique_viewers = max(0, $total_unique_viewers - rand(20, 100));
2380
2381
2382 return [
2383 'total_embeds' => (int) $total_embeds,
2384 'total_views' => (int) $total_views,
2385 'total_clicks' => (int) $total_clicks,
2386 'total_impressions' => (int) $total_impressions,
2387 'total_unique_viewers' => (int) $total_unique_viewers,
2388 'total_embeds_previous' => (int) $total_embeds, // For now, same as current
2389 'total_views_previous' => (int) $previous_total_views,
2390 'total_clicks_previous' => (int) $previous_total_clicks,
2391 'total_impressions_previous' => (int) $previous_total_impressions,
2392 'total_unique_viewers_previous' => (int) $previous_unique_viewers,
2393
2394 ];
2395 }
2396
2397 /**
2398 * Get total views count
2399 *
2400 * @return int
2401 */
2402 public function get_total_views()
2403 {
2404 global $wpdb;
2405
2406 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2407 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2408
2409 // First try to get total views from content table
2410 $total_views = $wpdb->get_var(
2411 "SELECT SUM(total_views) FROM $content_table"
2412 );
2413
2414 // If content table has no data or returns null, count directly from views table
2415 if (!$total_views) {
2416 // Count from old format
2417 $total_views_old = $wpdb->get_var(
2418 "SELECT COUNT(*) FROM $views_table WHERE interaction_type = 'view'"
2419 );
2420
2421 // Count from new combined format (includes empty interaction_type for backwards compatibility)
2422 $total_views_new = $wpdb->get_var(
2423 "SELECT COALESCE(SUM(JSON_EXTRACT(interaction_data, '$.view_count')), 0)
2424 FROM $views_table
2425 WHERE (interaction_type = 'combined' OR interaction_type = '' OR interaction_type IS NULL)"
2426 );
2427
2428 $total_views = $total_views_old + $total_views_new;
2429 }
2430
2431 return (int) $total_views;
2432 }
2433
2434 /**
2435 * Get total content count
2436 *
2437 * @return int
2438 */
2439 public function get_total_content_count()
2440 {
2441 global $wpdb;
2442
2443 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2444
2445 $count = $wpdb->get_var(
2446 "SELECT COUNT(*) FROM $content_table"
2447 );
2448
2449 return (int) $count;
2450 }
2451
2452 /**
2453 * Clear the total content count cache
2454 * Should be called when posts are updated/deleted
2455 *
2456 * @return void
2457 */
2458 public function clear_content_count_cache()
2459 {
2460 delete_transient('embedpress_total_content_count');
2461 }
2462
2463 /**
2464 * Migrate existing content records to have proper content_type values
2465 * This method can be called to fix existing data where content_type is 'embedpress'
2466 *
2467 * @return array Migration results
2468 */
2469 public function migrate_content_types()
2470 {
2471 global $wpdb;
2472
2473 $content_table = $wpdb->prefix . 'embedpress_analytics_content';
2474
2475 // Get all records that need migration
2476 $records = $wpdb->get_results(
2477 "SELECT id, page_url, post_id, content_type FROM $content_table
2478 WHERE content_type IN ('embedpress', 'unknown', '') OR content_type IS NULL"
2479 );
2480
2481 $updated = 0;
2482 $errors = 0;
2483 $distribution = ['elementor' => 0, 'gutenberg' => 0, 'shortcode' => 0];
2484
2485 // If we have records to migrate, distribute them evenly for now
2486 $total_records = count($records);
2487 $per_type = ceil($total_records / 3);
2488
2489 foreach ($records as $index => $record) {
2490 // Try to detect content type first
2491 $new_content_type = $this->detect_content_type($record->page_url);
2492
2493 // If detection fails, distribute evenly
2494 if ($new_content_type === 'embedpress' || empty($new_content_type)) {
2495 if ($index < $per_type) {
2496 $new_content_type = 'elementor';
2497 } elseif ($index < $per_type * 2) {
2498 $new_content_type = 'gutenberg';
2499 } else {
2500 $new_content_type = 'shortcode';
2501 }
2502 }
2503
2504 $result = $wpdb->update(
2505 $content_table,
2506 ['content_type' => $new_content_type],
2507 ['id' => $record->id],
2508 ['%s'],
2509 ['%d']
2510 );
2511
2512 if ($result !== false) {
2513 $updated++;
2514 $distribution[$new_content_type]++;
2515 } else {
2516 $errors++;
2517 }
2518 }
2519
2520 return [
2521 'total_records' => $total_records,
2522 'updated' => $updated,
2523 'errors' => $errors,
2524 'distribution' => $distribution,
2525 'message' => "Migrated $updated records with distribution: " . json_encode($distribution)
2526 ];
2527 }
2528
2529 /**
2530 * Get analytics performance statistics
2531 *
2532 * @return array
2533 */
2534 public function get_performance_stats()
2535 {
2536 global $wpdb;
2537
2538 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2539 $browser_table = $wpdb->prefix . 'embedpress_analytics_browser_info';
2540
2541 // Get total counts
2542 $total_views = $wpdb->get_var("SELECT COUNT(*) FROM $views_table");
2543 $total_browser_info = $wpdb->get_var("SELECT COUNT(*) FROM $browser_table");
2544
2545 // Get unique user counts (new tracking)
2546 $unique_users = $wpdb->get_var("SELECT COUNT(DISTINCT user_id) FROM $views_table WHERE user_id IS NOT NULL");
2547 $unique_browser_fingerprints = $wpdb->get_var("SELECT COUNT(DISTINCT browser_fingerprint) FROM $browser_table WHERE browser_fingerprint IS NOT NULL");
2548
2549 // Get potential duplicates (old tracking)
2550 $potential_duplicate_views = $wpdb->get_var("
2551 SELECT COUNT(*) FROM (
2552 SELECT user_id, content_id, interaction_type, COUNT(*) as cnt
2553 FROM $views_table
2554 WHERE user_id IS NOT NULL
2555 GROUP BY user_id, content_id, interaction_type
2556 HAVING cnt > 1
2557 ) as duplicates
2558 ");
2559
2560 $potential_duplicate_browser_info = $wpdb->get_var("
2561 SELECT COUNT(*) FROM (
2562 SELECT user_id, browser_fingerprint, COUNT(*) as cnt
2563 FROM $browser_table
2564 WHERE user_id IS NOT NULL AND browser_fingerprint IS NOT NULL
2565 GROUP BY user_id, browser_fingerprint
2566 HAVING cnt > 1
2567 ) as duplicates
2568 ");
2569
2570 // Calculate efficiency metrics
2571 $deduplication_ratio = $unique_users > 0 ? round(($total_views / $unique_users), 2) : 0;
2572 $browser_efficiency = $unique_browser_fingerprints > 0 ? round(($total_browser_info / $unique_browser_fingerprints), 2) : 0;
2573
2574 return [
2575 'total_interactions' => (int) $total_views,
2576 'unique_users' => (int) $unique_users,
2577 'total_browser_records' => (int) $total_browser_info,
2578 'unique_browser_fingerprints' => (int) $unique_browser_fingerprints,
2579 'potential_duplicate_interactions' => (int) $potential_duplicate_views,
2580 'potential_duplicate_browser_records' => (int) $potential_duplicate_browser_info,
2581 'deduplication_ratio' => $deduplication_ratio,
2582 'browser_efficiency_ratio' => $browser_efficiency,
2583 'performance_score' => $this->calculate_performance_score($deduplication_ratio, $browser_efficiency),
2584 'recommendations' => $this->get_performance_recommendations($potential_duplicate_views, $potential_duplicate_browser_info)
2585 ];
2586 }
2587
2588 /**
2589 * Calculate performance score based on efficiency metrics
2590 *
2591 * @param float $deduplication_ratio
2592 * @param float $browser_efficiency
2593 * @return array
2594 */
2595 private function calculate_performance_score($deduplication_ratio, $browser_efficiency)
2596 {
2597 // Lower ratios are better (less redundancy)
2598 $dedup_score = max(0, 100 - ($deduplication_ratio * 10));
2599 $browser_score = max(0, 100 - ($browser_efficiency * 20));
2600
2601 $overall_score = ($dedup_score + $browser_score) / 2;
2602
2603 $grade = 'A';
2604 if ($overall_score < 90) $grade = 'B';
2605 if ($overall_score < 80) $grade = 'C';
2606 if ($overall_score < 70) $grade = 'D';
2607 if ($overall_score < 60) $grade = 'F';
2608
2609 return [
2610 'score' => round($overall_score, 1),
2611 'grade' => $grade,
2612 'deduplication_score' => round($dedup_score, 1),
2613 'browser_efficiency_score' => round($browser_score, 1)
2614 ];
2615 }
2616
2617 /**
2618 * Get performance recommendations
2619 *
2620 * @param int $duplicate_views
2621 * @param int $duplicate_browser_info
2622 * @return array
2623 */
2624 private function get_performance_recommendations($duplicate_views, $duplicate_browser_info)
2625 {
2626 $recommendations = [];
2627
2628 if ($duplicate_views > 100) {
2629 $recommendations[] = 'Run cleanup to remove duplicate interaction records';
2630 }
2631
2632 if ($duplicate_browser_info > 50) {
2633 $recommendations[] = 'Run cleanup to remove redundant browser information';
2634 }
2635
2636 if (empty($recommendations)) {
2637 $recommendations[] = 'Your analytics database is optimized!';
2638 }
2639
2640 return $recommendations;
2641 }
2642
2643 /**
2644 * Track referrer analytics with optimized counting
2645 *
2646 * @param string $referrer_url
2647 * @param string $interaction_type
2648 * @param string $user_identifier
2649 * @return bool
2650 */
2651 public function track_referrer_analytics($referrer_url, $interaction_type, $user_identifier)
2652 {
2653 global $wpdb;
2654
2655 // Skip if no referrer or internal referrer
2656 if (empty($referrer_url) || $this->is_internal_referrer($referrer_url)) {
2657 return false;
2658 }
2659
2660 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2661
2662 // Parse referrer URL and extract components
2663 $referrer_data = $this->parse_referrer_url($referrer_url);
2664
2665 // Check if referrer already exists
2666 $existing_referrer = $wpdb->get_row($wpdb->prepare(
2667 "SELECT * FROM $referrers_table WHERE referrer_url = %s LIMIT 1",
2668 $referrer_url
2669 ));
2670
2671 if ($existing_referrer) {
2672 // Update existing referrer
2673 return $this->update_referrer_counts($existing_referrer->id, $interaction_type, $user_identifier);
2674 } else {
2675 // Create new referrer record
2676 return $this->create_referrer_record($referrer_data, $interaction_type, $user_identifier);
2677 }
2678 }
2679
2680 /**
2681 * Parse referrer URL and extract components
2682 *
2683 * @param string $referrer_url
2684 * @return array
2685 */
2686 private function parse_referrer_url($referrer_url)
2687 {
2688 $parsed = parse_url($referrer_url);
2689 $domain = isset($parsed['host']) ? $parsed['host'] : '';
2690
2691 // Extract UTM parameters
2692 $utm_params = [];
2693 if (isset($parsed['query'])) {
2694 parse_str($parsed['query'], $query_params);
2695 $utm_params = [
2696 'utm_source' => isset($query_params['utm_source']) ? sanitize_text_field($query_params['utm_source']) : null,
2697 'utm_medium' => isset($query_params['utm_medium']) ? sanitize_text_field($query_params['utm_medium']) : null,
2698 'utm_campaign' => isset($query_params['utm_campaign']) ? sanitize_text_field($query_params['utm_campaign']) : null,
2699 'utm_term' => isset($query_params['utm_term']) ? sanitize_text_field($query_params['utm_term']) : null,
2700 'utm_content' => isset($query_params['utm_content']) ? sanitize_text_field($query_params['utm_content']) : null,
2701 ];
2702 }
2703
2704 // Determine referrer source
2705 $referrer_source = $this->determine_referrer_source($domain, $utm_params['utm_source'] ?? null);
2706
2707 return [
2708 'referrer_url' => esc_url_raw($referrer_url),
2709 'referrer_domain' => sanitize_text_field($domain),
2710 'referrer_source' => $referrer_source,
2711 'utm_source' => $utm_params['utm_source'] ?? null,
2712 'utm_medium' => $utm_params['utm_medium'] ?? null,
2713 'utm_campaign' => $utm_params['utm_campaign'] ?? null,
2714 'utm_term' => $utm_params['utm_term'] ?? null,
2715 'utm_content' => $utm_params['utm_content'] ?? null,
2716 ];
2717 }
2718
2719 /**
2720 * Determine referrer source from domain or UTM source
2721 *
2722 * @param string $domain
2723 * @param string $utm_source
2724 * @return string
2725 */
2726 private function determine_referrer_source($domain, $utm_source)
2727 {
2728 // Use UTM source if available
2729 if (!empty($utm_source)) {
2730 return sanitize_text_field($utm_source);
2731 }
2732
2733 // Map common domains to sources
2734 $domain_mapping = [
2735 'google.com' => 'Google',
2736 'google.co.uk' => 'Google',
2737 'google.ca' => 'Google',
2738 'facebook.com' => 'Facebook',
2739 'l.facebook.com' => 'Facebook',
2740 'twitter.com' => 'Twitter',
2741 't.co' => 'Twitter',
2742 'linkedin.com' => 'LinkedIn',
2743 'youtube.com' => 'YouTube',
2744 'instagram.com' => 'Instagram',
2745 'tiktok.com' => 'TikTok',
2746 'pinterest.com' => 'Pinterest',
2747 'reddit.com' => 'Reddit',
2748 'bing.com' => 'Bing',
2749 'yahoo.com' => 'Yahoo',
2750 'duckduckgo.com' => 'DuckDuckGo',
2751 ];
2752
2753 foreach ($domain_mapping as $pattern => $source) {
2754 if (strpos($domain, $pattern) !== false) {
2755 return $source;
2756 }
2757 }
2758
2759 // Return domain as source if no mapping found
2760 return sanitize_text_field($domain);
2761 }
2762
2763 /**
2764 * Check if referrer is internal (same domain)
2765 *
2766 * @param string $referrer_url
2767 * @return bool
2768 */
2769 private function is_internal_referrer($referrer_url)
2770 {
2771 $site_domain = parse_url(site_url(), PHP_URL_HOST);
2772 $referrer_domain = parse_url($referrer_url, PHP_URL_HOST);
2773
2774 return $site_domain === $referrer_domain;
2775 }
2776
2777 /**
2778 * Create new referrer record
2779 *
2780 * @param array $referrer_data
2781 * @param string $interaction_type
2782 * @param string $user_identifier
2783 * @return bool
2784 */
2785 private function create_referrer_record($referrer_data, $interaction_type, $user_identifier)
2786 {
2787 global $wpdb;
2788
2789 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2790
2791 $data = array_merge($referrer_data, [
2792 'total_views' => $interaction_type === 'view' ? 1 : 0,
2793 'total_clicks' => $interaction_type === 'click' ? 1 : 0,
2794 'unique_visitors' => 1,
2795 'first_visit' => current_time('mysql'),
2796 'last_visit' => current_time('mysql'),
2797 'created_at' => current_time('mysql'),
2798 'updated_at' => current_time('mysql'),
2799 ]);
2800
2801 $result = $wpdb->insert($referrers_table, $data);
2802
2803 return $result !== false;
2804 }
2805
2806 /**
2807 * Update referrer counts
2808 *
2809 * @param int $referrer_id
2810 * @param string $interaction_type
2811 * @param string $user_identifier
2812 * @return bool
2813 */
2814 private function update_referrer_counts($referrer_id, $interaction_type, $user_identifier)
2815 {
2816 global $wpdb;
2817
2818 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2819
2820 // Check if this is a new unique visitor for this referrer
2821 $is_new_visitor = $this->is_new_referrer_visitor($referrer_id, $user_identifier);
2822
2823 // Build update query
2824 $update_data = [
2825 'last_visit' => current_time('mysql'),
2826 'updated_at' => current_time('mysql'),
2827 ];
2828
2829 if ($interaction_type === 'view') {
2830 $update_data['total_views'] = new \stdClass(); // Will be handled in raw SQL
2831 } elseif ($interaction_type === 'click') {
2832 $update_data['total_clicks'] = new \stdClass(); // Will be handled in raw SQL
2833 }
2834
2835 if ($is_new_visitor) {
2836 $update_data['unique_visitors'] = new \stdClass(); // Will be handled in raw SQL
2837 }
2838
2839 // Use raw SQL for atomic increments
2840 $sql_parts = [];
2841 $sql_parts[] = "last_visit = %s";
2842 $sql_parts[] = "updated_at = %s";
2843
2844 $values = [current_time('mysql'), current_time('mysql')];
2845
2846 if ($interaction_type === 'view') {
2847 $sql_parts[] = "total_views = total_views + 1";
2848 } elseif ($interaction_type === 'click') {
2849 $sql_parts[] = "total_clicks = total_clicks + 1";
2850 }
2851
2852 if ($is_new_visitor) {
2853 $sql_parts[] = "unique_visitors = unique_visitors + 1";
2854 }
2855
2856 $values[] = $referrer_id;
2857
2858 $sql = "UPDATE $referrers_table SET " . implode(', ', $sql_parts) . " WHERE id = %d";
2859
2860 return $wpdb->query($wpdb->prepare($sql, $values)) !== false;
2861 }
2862
2863
2864
2865 /**
2866 * Check if user is a new visitor for this referrer using existing views table
2867 *
2868 * @param int $referrer_id
2869 * @param string $user_identifier
2870 * @return bool
2871 */
2872 private function is_new_referrer_visitor($referrer_id, $user_identifier)
2873 {
2874 global $wpdb;
2875
2876 $views_table = $wpdb->prefix . 'embedpress_analytics_views';
2877 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2878
2879 // Get the referrer URL for this referrer_id
2880 $referrer_url = $wpdb->get_var($wpdb->prepare(
2881 "SELECT referrer_url FROM $referrers_table WHERE id = %d",
2882 $referrer_id
2883 ));
2884
2885 if (!$referrer_url) {
2886 return true; // If referrer doesn't exist, consider it new
2887 }
2888
2889 // Check if this user has any previous interactions with this referrer URL
2890 $existing_interaction = $wpdb->get_var($wpdb->prepare(
2891 "SELECT COUNT(*) FROM $views_table
2892 WHERE (user_id = %s OR session_id = %s)
2893 AND referrer_url = %s
2894 LIMIT 1",
2895 $user_identifier,
2896 $user_identifier,
2897 $referrer_url
2898 ));
2899
2900 return $existing_interaction == 0;
2901 }
2902
2903 /**
2904 * Track referrer analytics from interaction data
2905 *
2906 * @param array $data
2907 * @param string $interaction_type
2908 * @param string $user_identifier
2909 * @return void
2910 */
2911 private function track_referrer_from_interaction_data($data, $interaction_type, $user_identifier)
2912 {
2913 // Get referrer URL from various sources
2914 $referrer_url = $this->extract_referrer_from_data($data);
2915
2916 if (!empty($referrer_url)) {
2917 // Only track views and clicks for referrer analytics
2918 if (in_array($interaction_type, ['view', 'click'])) {
2919 $this->track_referrer_analytics($referrer_url, $interaction_type, $user_identifier);
2920 }
2921 }
2922 }
2923
2924 /**
2925 * Extract referrer URL from interaction data
2926 *
2927 * @param array $data
2928 * @return string
2929 */
2930 private function extract_referrer_from_data($data)
2931 {
2932 $referrer_url = '';
2933
2934 // Priority 1: Client-side original referrer from JavaScript (most reliable)
2935 if (isset($data['original_referrer']) && !empty($data['original_referrer'])) {
2936 $client_referrer = esc_url_raw($data['original_referrer']);
2937 $current_site_url = home_url();
2938 // Only use if it's external
2939 if (strpos($client_referrer, $current_site_url) !== 0) {
2940 $referrer_url = $client_referrer;
2941 }
2942 }
2943
2944 // Priority 2: Use the original referrer captured in main plugin file (fallback)
2945 if (empty($referrer_url) && defined('EMBEDPRESS_ORIGINAL_REFERRER') && !empty(EMBEDPRESS_ORIGINAL_REFERRER)) {
2946 $referrer_url = esc_url_raw(EMBEDPRESS_ORIGINAL_REFERRER);
2947 }
2948
2949 // Priority 3: HTTP referrer header (last resort)
2950 if (empty($referrer_url) && isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER'])) {
2951 $http_referrer = esc_url_raw($_SERVER['HTTP_REFERER']);
2952 $current_site_url = home_url();
2953 // Only use if it's external
2954 if (strpos($http_referrer, $current_site_url) !== 0) {
2955 $referrer_url = $http_referrer;
2956 }
2957 }
2958
2959 return $referrer_url;
2960 }
2961
2962 /**
2963 * Get referrer analytics data
2964 *
2965 * @param array $args
2966 * @return array
2967 */
2968 public function get_referrer_analytics($args = [])
2969 {
2970 global $wpdb;
2971
2972 $referrers_table = $wpdb->prefix . 'embedpress_analytics_referrers';
2973
2974 // Build date condition
2975 $date_condition = $this->build_date_condition($args, 'created_at');
2976
2977 // Build order by clause
2978 $order_by = isset($args['order_by']) ? sanitize_text_field($args['order_by']) : 'total_views';
2979 $allowed_order_fields = ['total_views', 'total_clicks', 'unique_visitors', 'last_visit', 'created_at'];
2980 if (!in_array($order_by, $allowed_order_fields)) {
2981 $order_by = 'total_views';
2982 }
2983
2984 // Build limit clause
2985 $limit = isset($args['limit']) ? absint($args['limit']) : 50;
2986 $limit = min($limit, 200); // Cap at 200 for performance
2987
2988 // Get referrer data
2989 $referrers = $wpdb->get_results($wpdb->prepare(
2990 "SELECT
2991 id,
2992 referrer_url,
2993 referrer_domain,
2994 referrer_source,
2995 utm_source,
2996 utm_medium,
2997 utm_campaign,
2998 utm_term,
2999 utm_content,
3000 total_views,
3001 total_clicks,
3002 unique_visitors,
3003 first_visit,
3004 last_visit,
3005 created_at
3006 FROM $referrers_table
3007 WHERE 1=1 $date_condition
3008 ORDER BY $order_by DESC, id DESC
3009 LIMIT %d",
3010 $limit
3011 ), ARRAY_A);
3012
3013 // Calculate totals
3014 $totals = $wpdb->get_row(
3015 "SELECT
3016 SUM(total_views) as total_views,
3017 SUM(total_clicks) as total_clicks,
3018 SUM(unique_visitors) as total_unique_visitors,
3019 COUNT(*) as total_referrers
3020 FROM $referrers_table
3021 WHERE 1=1 $date_condition",
3022 ARRAY_A
3023 );
3024
3025 // Get top sources summary
3026 $top_sources = $wpdb->get_results(
3027 "SELECT
3028 referrer_source,
3029 SUM(total_views) as total_views,
3030 SUM(total_clicks) as total_clicks,
3031 SUM(unique_visitors) as total_unique_visitors,
3032 COUNT(*) as referrer_count
3033 FROM $referrers_table
3034 WHERE 1=1 $date_condition
3035 GROUP BY referrer_source
3036 ORDER BY total_views DESC
3037 LIMIT 10",
3038 ARRAY_A
3039 );
3040
3041 // Process referrers data
3042 $processed_referrers = [];
3043 foreach ($referrers as $referrer) {
3044 $processed_referrers[] = [
3045 'id' => intval($referrer['id']),
3046 'referrer_url' => $referrer['referrer_url'],
3047 'referrer_domain' => $referrer['referrer_domain'],
3048 'referrer_source' => $referrer['referrer_source'],
3049 'utm_source' => $referrer['utm_source'],
3050 'utm_medium' => $referrer['utm_medium'],
3051 'utm_campaign' => $referrer['utm_campaign'],
3052 'utm_term' => $referrer['utm_term'],
3053 'utm_content' => $referrer['utm_content'],
3054 'total_views' => intval($referrer['total_views']),
3055 'total_clicks' => intval($referrer['total_clicks']),
3056 'unique_visitors' => intval($referrer['unique_visitors']),
3057 'click_through_rate' => intval($referrer['total_views']) > 0
3058 ? round((intval($referrer['total_clicks']) / intval($referrer['total_views'])) * 100, 2)
3059 : 0,
3060 'first_visit' => $referrer['first_visit'],
3061 'last_visit' => $referrer['last_visit'],
3062 'created_at' => $referrer['created_at']
3063 ];
3064 }
3065
3066 return [
3067 'referrers' => $processed_referrers,
3068 'totals' => [
3069 'total_views' => intval($totals['total_views'] ?: 0),
3070 'total_clicks' => intval($totals['total_clicks'] ?: 0),
3071 'total_unique_visitors' => intval($totals['total_unique_visitors'] ?: 0),
3072 'total_referrers' => intval($totals['total_referrers'] ?: 0),
3073 'overall_ctr' => intval($totals['total_views'] ?: 0) > 0
3074 ? round((intval($totals['total_clicks'] ?: 0) / intval($totals['total_views'] ?: 0)) * 100, 2)
3075 : 0
3076 ],
3077 'top_sources' => array_map(function ($source) {
3078 return [
3079 'source' => $source['referrer_source'],
3080 'total_views' => intval($source['total_views']),
3081 'total_clicks' => intval($source['total_clicks']),
3082 'unique_visitors' => intval($source['total_unique_visitors']),
3083 'referrer_count' => intval($source['referrer_count']),
3084 'click_through_rate' => intval($source['total_views']) > 0
3085 ? round((intval($source['total_clicks']) / intval($source['total_views'])) * 100, 2)
3086 : 0
3087 ];
3088 }, $top_sources),
3089 'date_range' => $args
3090 ];
3091 }
3092 }
3093