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